代码审计

php函数绕过小结

0.preg_replace()

该函数使用正则表达式来进行匹配特定的字符串.

  • 问题代码
1
2
3
4
5
6
7
<?php
$str = addslashes($_GET['option']);
$file = file_get_contents('xxxxx/option.php');
$file = preg_replace('|\$option=\'.*\';|',"\$option='$str';",$file);
file_put_contents('xxxxx/option.php',$file);

?>

输入经过addslashes()处理过之后经匹配替换指定文件内容。

  • 解法1 利用反斜线

输入\';phpinfo();//
经过addslashes()之后变为\',随后preg_replace会将两个连续的\合并为一个,也就是将\'转为\‘,这样我们就成功引入了一个单引号,闭合上文注释下文,中间加入要执行的代码即可.

  • 解法2 利用正则
    过程分为两个请求:

第一次传入 aaa’;phpinfo();%0a//

此时文件内容

$option=’aaa';phpinfo();
//‘;
第二次传入随意字串,如bbb 正则代码.*会将匹配到的aaa\替换为bbb

此时文件内容(成功写入恶意代码)

$option=’bbb’;phpinfo();
//‘;

  • 解法3 利用%00
    仍然分为两步。

第一次传入;phpinfo(); 此时文件内容为:

1
$option=';phpinfo();';

第二次传入%00

%00被addslashes()转为\0,而\0在preg_replace函数中会被替换为“匹配到的全部内容”,此时preg_replace要执行的代码如下

1
preg_replace('|\$option=\'.*\';|',"\$option='\0';",$file);

也就是

1
preg_replace('|\$option=\'.*\';|',"\$option='$option=';phpinfo();';';",$file);

成功引入单引号闭合,最终写入shell

1
2

$option='\$option=';phpinfo();';';

1.MD5()

我们都知道,MD5 加密(sha1())是对字符串进行加密,那么如果我们传入的不是字符串,而是一个数组呢??? 它没法进行加密,返回空,结果不就相等了吗.(大部分的php函数,如strcmp()无法处理数组,返回值基本是NULL).

2.str_replace()

str_replace()函数用于过滤多余字符的,基本可以通过双写目标字符来解决,例如../可以用….//或者…/./来解决,key可以用kkeyey来解决.

3.parse_url()

本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
对于高版本的php的来说 直接/// 三个斜杠就可以直接解决.

4.intval()

intval()转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。
即使出现无法转换的字符串,intval()不会报错而是返回0。
注:
在科学计数法字符串转换为数字时,如果 E 后面的数小于某个值会弄成 double 类型,再强制转换为 int 类型时可能会有奇妙的结果,测试发现某变量为 1e-1000 时已经可以触发这个漏洞绕过两个检查,使得某变量既大于 0 又不大于 0。
例如:

1
2
var_dump((int)('1e-1000')>0);
var_dump('1e-1000'>0);

结果

1
2
3
4
Command line code:1:
bool(true)
Command line code:1:
bool(false)

再如:

1
2
var_dump((int)('1e-10')>0);
var_dump('1e-10'>0);

结果

1
2
3
4
Command line code:1:
bool(true)
Command line code:1:
bool(true)

用到了PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型intval再比.
当检索中带入字符串,比如”sky”,会intval(‘sky’)==0,从而致使数字数组也可以查询成功.

6.switch

没有break的话不用判断真假即进入下一个case.

7.php://filter协议

php://filter 这个协议可以用来读取网页base64编码后的源代码。用这句 file=php://filter/read=convert.base64-encode/resource=xxxx.php.

8.附:BUgku平台一道代码审计题

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
<php?
if(!$_GET['id'])
{
header('Location: hello.php?id=1');
exit();
}
id=_GET['id'];
a=_GET['a'];
b=_GET['b'];
if(stripos($a,'.'))
{
echo 'no no no no no no no';
return ;
}
data=@filegetcontents(a,'r');
if($data=="bugku is a nice plateform!" and id==0andstrlen(b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
require("f4l2a3g.txt");
}
else
{
print "never never never give up !!!";
}


?>

变量 $id 若想满足非空非零且弱等于整型数 0,则 $id 的值只能为非空非零字符串,这里假设 $id = “asd”.

源码中变量 $data 是由file_get_contents() 函数读取变量 $a 的值而得,所以 $a 的值必须为数据流.在服务器中自定义一个内容为 bugku is a nice plateform! 文件,再把此文件路径赋值给 $a,显然不太现实.因此这里用伪协议 php:// 来访问输入输出的数据流,其中php://input可以访问原始请求数据中的只读流.这里令 $a = “php://input”,并在请求主体中提交字符串 bugku is a nice plateform!.

ereg() 函数或 eregi() 函数存在空字符截断漏洞,即参数中的正则表达式或待匹配字符串遇到空字符则截断丢弃后面的数据。源码中待匹配字符串(第二个参数)已确定为 “1114”,正则表达式(第一个参数)由 “111” 连接 $b 的第一个字符组成,若令 substr($b,0,1) = “\x00”,即满足 “1114” 与 “111”匹配。因此,这里假设 $b = “\x0012345”.

代码审计

HCTF 2018 Warmup

  • 这个题目原型是phpmyadmin4.8.1的任意文件包含漏洞

  • 点击hint进入,得到提示flag在ffffllllaaaagggg中,并发现URL格式为XXX/index.php?file=hint.php,顺势猜一下file=source.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
    <?php
    class emmm
    {
    public static function checkFile(&$page)
    {
    $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
    if (! isset($page) || !is_string($page)) {
    echo "you can't see it";
    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;
    }
    echo "you can't see it";
    return false;
    }
    }

    if (! empty($_REQUEST['file'])
    && is_string($_REQUEST['file'])
    && emmm::checkFile($_REQUEST['file'])
    ) {
    include $_REQUEST['file'];
    exit;
    } else {
    echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }
    ?>
  • 首先看这一段:

    1
    2
    3
    4
    if (! empty($_REQUEST['file'])
    && is_string($_REQUEST['file'])
    && emmm::checkFile($_REQUEST['file'])
    )

也就是说checkfile的返回值要非空,就会将文件包含.

  • 接下来看这段:
    1
    2
    3
    4
    5
    $_page = mb_substr(
    $page,
    0,
    mb_strpos($page . '?', '?')
    );

这段代码的意思是将post上的数据后面加?,然后提取出?之前的内容.

  • 这一段:
    1
    2
    3
    4
    5
    6
    $_page = urldecode($page);
    $_page = mb_substr(
    $_page,
    0,
    mb_strpos($_page . '?', '?')
    );

首先对第一次提取出来的内容进行url编码,然后进行第二次提取.第二次提取出来的内容在白名单之内即返回true.

  • 最终payload:file=source.php%253f/../../../../../ffffllllaaaagggg
    第一次提取:source.php%3f/../../../../../ffffllllaaaagggg
    url编码后:source.php?/../../../../../ffffllllaaaagggg
    第二次提取:source.php,在白名单中则返回true.
    但是在进行文件包含的时候依然会是原字符串,source.php%3f是个不存在的目录,依然不会影响.

    BUUCTF easy_calc(国赛love_math)

  • 看一下页面源码,发现了提示:
    1
    calc.php?num=encodeURIComponent($("#content").val())

$(“#content”).val() 是什么意思:

获取id为content的HTML标签元素的值,是JQuery, $(“#content”)相当于document.getElementById(“content”); $(“#content”).val()相当于 document.getElementById(“content”).value;

但是无论怎么注入都是400,403和500,这里用的是一个新的点:PHP的字符串解析特性:
我们知道PHP将查询字符串(在URL或正文中)转换为内部$ _GET或关联的关联$ _POST。例如:/?foo = bar变成Array([foo] =>“ bar”)。是,查询字符串在解析的过程中可以被某些字符删除或用下划线代替。例如,/?%20news [id%00 = 42会转换为Array([news_id] => 42)。在解析查询字符串时,它会做两件事:1.删​​除空白符2.将某些字符转换为下划线(包括空格).

  • 扫一下根目录,发现flagg文件:?%20num=1;var_dump(scandir(chr(47)))

  • 列出flagg:?%20num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

    De1CTF ssrf_me

  • 直接查看页面源代码可以看到正确格式的代码:

    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
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    #! /usr/bin/env python
    #encoding=utf-8
    from flask import Flask
    from flask import request
    import socket
    import hashlib
    import urllib
    import sys
    import os
    import json

    reload(sys)
    sys.setdefaultencoding('latin1')

    app = Flask(__name__)

    secert_key = os.urandom(16)

    class Task:
    def __init__(self, action, param, sign, ip):
    self.action = action
    self.param = param
    self.sign = sign
    self.sandbox = md5(ip)
    if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
    os.mkdir(self.sandbox)

    def Exec(self):
    result = {}
    result['code'] = 500
    if (self.checkSign()):
    if "scan" in self.action:
    tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
    resp = scan(self.param)
    if (resp == "Connection Timeout"):
    result['data'] = resp
    else:
    print(resp)
    tmpfile.write(resp)
    tmpfile.close()
    result['code'] = 200
    if "read" in self.action:
    f = open("./%s/result.txt" % self.sandbox, 'r')
    result['code'] = 200
    result['data'] = f.read()
    if result['code'] == 500:
    result['data'] = "Action Error"
    else:
    result['code'] = 500
    result['msg'] = "Sign Error"
    return result

    def checkSign(self):
    if (getSign(self.action, self.param) == self.sign):
    return True
    else:
    return False

    #generate Sign For Action Scan.
    @app.route("/geneSign", methods=['GET', 'POST'])
    def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

    @app.route('/De1ta',methods=['GET','POST'])
    def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
    return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())
    @app.route('/')
    def index():
    return open("code.txt","r").read()

    def scan(param):
    socket.setdefaulttimeout(1)
    try:
    return urllib.urlopen(param).read()[:50]
    except:
    return "Connection Timeout"

    def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

    def md5(content):
    return hashlib.md5(content).hexdigest()

    def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
    return True
    else:
    return False

    if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=80)
  • 提示给的是 flag 在 ./flag.txt 中,题目单词打错了.

  • python 的 flask 框架,三个路由,index 用于获取源码,geneSign 用于生成 md5,De1ta 就是挑战

  • 大概思路就是在 /De1ta 中 get param ,cookie action sign 去读取 flag.txt,其中,param=flag.txt,action 中要含有 read 和 scan,且 sign=md5(secert_key + param + action)

  • 试着访问了一下 /geneSign?param=flag.txt ,给出了一个 md5 f36ac37ddfdcc567b5e4bfafd989672e ,但是只有 scan 的功能,想加入 read 功能就要另想办法了:

    1
    2
    3
    4
    def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
  • 看了一下逻辑,在 getSign 处很有意思,这个字符串拼接的就很有意思了

    1
    2
    def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()
  • 不妨假设 secert_key 是 xxx ,那么在开始访问 /geneSign?param=flag.txt 的时候,返回的 md5 就是 md5(‘xxx’ + ‘flag.txt’ + ‘scan’) ,在 python 里面上述表达式就相当于 md5(xxxflag.txtscan) ,这就很有意思了。

  • 直接构造访问 /geneSign?param=flag.txtread ,拿到的 md5 就是 md5(‘xxx’ + ‘flag.txtread’ + ‘scan’) ,等价于 md5(‘xxxflag.txtreadscan’) ,这就达到了目标。

0%