IgnatiusHeo

구현단계 보안약점 제거 기준-XML 삽입 본문

자격/SW보안약점진단원

구현단계 보안약점 제거 기준-XML 삽입

Ignatius Heo 2023. 7. 4. 04:35

작성일: 230704

 

※ 본 게시글은 학습 목적으로 행정안전부·KISA의 소프트웨어 보안약점 진단 가이드, 소프트웨어 개발보안 가이드를 참고하여 작성하였습니다.

 

정리 내용: 소프트웨어 보안약점 진단 가이드(251~263p)

 

구분 - 입력데이터 검증 및 표현
설계단계 - XML 조회 및 결과 검증
https://cryptocurrencyclub.tistory.com/90
개요
검증되지 않은 외부 입력 값이 XQuery 또는 XPath 쿼리문을 생성하는 문자열로 사용되어 공격자가 쿼리문의 구조로 임의로 변경하고 임의의 쿼리를 실행하여 허가되지 않은 데이터를 열람하거나 인증절차를 우회할 수 있는 보안약점이다.

진단 세부사항
(설계단계)
 
 ① XML문서를 조회하는 기능을 구현하는 경우 XML쿼리 파라미터는 반드시 쿼리를 조작할 수 없도록 필터링해서 사용하거나, 미리 작성된 질의문에 입력값을 자료형에 따라 바인딩해서 사용해야 한다. 또한 XML 조회를 위한 질의문 생성 시 사용되는 입력값과 조회결과에 대한 검증방법(필터링 등)을 설계하고 유효하지 않은 값에 대한 처리방법이 명시되어 있는지 확인한다.
 
  ㅇ  XML 조회에 사용되는 외부 입력값을 안전하게 필터링하는 기능이 설계되어 있거나 안전한 외부라이브러리를 사용하도록 설계되어 있는지 확인
  ㅇ  XML데이터를 조회하는 기능 구현시, XML필터링을 적용하기 위한 코딩규칙이나 안전하게 사용할 수 있는 API에 대한 설명이 개발가이드에 정의되어 있는지 확인
  ㅇ XML조회 구문을 변경할 수 있는 입력값을 사용하여 XML조회 구문이 변경되는지를 점검하는 테스트계획의 수립 여부 확인
    → 구문 변경 가능 입력값: 쿼리예약어, * [ ] / = @
 
보안대책
(구현단계)

XQuery 또는 Xpath 쿼리에 사용되는 외부 입력데이터에 대하여 특수문자 및 쿼리 예약어를 필터링하고 파라미터화된 쿼리문을 지원하는 XQuery를 사용한다.

진단방법
(구현단계)
- XQuery 삽입
① XQuery가 실행되는 부분을 확인하고
② XQuery 쿼리스트링에 사용되는 변수가 외부 입력값 여부를 확인한 후, 변수에 대한 필터링 모듈이 존재하는지 확인한다.

필터링 모듈이 존재하거나 관련 프레임워크에서 적절히 조치할 경우 안전한 것으로 판정한다.

- XPath 삽입
① XPath 객체로 쿼리 스트링이 컴파일 되는 부분을 확인하고,
② XPath 쿼리스트링에 사용되는 변수가 외부 입력값 인지 확인한 후 변수에 대한 필터링 모듈이 존재하는지 확인한다.

필터링 모듈이 존재하거나 관련 프레임워크에서 적절하게 조치된 경우엔 안전하다고 판정한다.

 

다. 코드예제


ㅇ 분석

 

1: 
2: String name = props.getProperty("name");
3: .......
4: 
5: String es = "doc('users.xml')/userlist/user[uname='"+name+"']";
6: XQPreparedExpression expr = conn.prepareExpression(es);
7: XQResultSequence result = expr.executeQuery();

ㅇ 설명

1. name 검증 어디갔어

 

ㅇ 수정

1: // 
2: String name = props.getProperty("name");
3: .......
4: String es = "doc('users.xml')/userlist/user[uname='$xname']";
5: XQPreparedExpression expr = conn.prepareExpression(es);
6: expr.bindString(new QName("xname"), name, null);
7: XQResultSequence result = expr.executeQuery();

ㅇ 설명

1. 외부로부터 받은 name과 xquery를 바인딩, 매핑해서 사용(sql에서 preparedstatment와 유사함)

 


ㅇ 분석

//
1:String squery =
2: "for $user in doc(users.xml)//user[username='"
3: + UserTextBox.Text
4: + "'and pass='"
5: + PwdTextBox.Text
6: + "'] return $user";
7:Processor processor = new Processor();
8:XQueryCompiler compiler = processor.NewXQueryCompiler();
9:XdmNode indoc = processor.NewDocumentBuilder().Build(new
Uri(Server.MapPath("users.xml")));
10: using (StreamReader query = new StreamReader(squery))
11: {
12: XQueryCompiler compiler = processor.NewXQueryCompiler();
13: XQueryExecutable exp = compiler.Compile(query.ReadToEnd());
14: XQueryEvaluator eval = exp.Load();
15: eval.ContextItem = indoc;
16: Serializer qout = new Serializer();
17: qout.SetOutputProperty(Serializer.METHOD, "xml");
18: qout.SetOutputProperty(Serializer.DOCTYPE_PUBLIC, "-//W3C//DTD XHTML
1.0 Strict//EN");
19: qout.SetOutputProperty(Serializer.DOCTYPE_SYSTEM,
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd");
20: qout.SetOutputProperty(Serializer.INDENT, "yes");
21: qout.SetOutputProperty(Serializer.OMIT_XML_DECLARATION, "no");
22: qout.SetOutputWriter(Response.Output);
//
23: eval.Run(qout);27:
}

ㅇ 내용

1. r3하고 r5에서 UserTextBox, PwdTextBox값을 직접 받아 squery를 만듬.

2. 직접 받은 값을 r10에서 Streamreader 객체 생성, 이후 일련의 처리 후 23에서 eval.Run(실행)을 하는데, 이 과정동안 검증 내용이 빠짐

 

ㅇ 수정

1:String squery =
2: "for $user in doc(users.xml)//user[username='"
3: + UserTextBox.Text
4: + "'and pass='"
5: + PwdTextBox.Text
6: + "'] return $user";
// 문자열 필터링으로 위험한 문자열을 제거해 줍니다.
7: string validatedQuery = squery.Replace('/','*');
8: Processor processor = new Processor();
9: XQueryCompiler compiler = processor.NewXQueryCompiler();
10: XdmNode indoc = processor.NewDocumentBuilder().Build(new
Uri(Server.MapPath("users.xml")));
11:using (StreamReader query = new StreamReader(validatedQuery))
12:{ // tainted value propagated
13: XQueryCompiler compiler = processor.NewXQueryCompiler();
14: XQueryExecutable exp = compiler.Compile(query.ReadToEnd()); //
xquery created
15: XQueryEvaluator eval = exp.Load();
16: eval.ContextItem = indoc;
17: Serializer qout = new Serializer();
18: qout.SetOutputProperty(Serializer.METHOD, "xml");
19: qout.SetOutputProperty(Serializer.DOCTYPE_PUBLIC, "-//W3C//DTD XHTML
1.0 Strict//EN");
20: qout.SetOutputProperty(Serializer.DOCTYPE_SYSTEM,
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd");
21: qout.SetOutputProperty(Serializer.INDENT, "yes");
22: qout.SetOutputProperty(Serializer.OMIT_XML_DECLARATION, "no");
23: qout.SetOutputWriter(Response.Output);
24: eval.Run(qout);
25:}

ㅇ 설명

1. r7에서 /를 *로 대치하는데.. *도 쿼리 변경 가능 문자 아닌가?


ㅇ 분석

// 
1:String nm = props.getProperty("name");
2:String pw = props.getProperty("password");
3:......
4:XPathFactory factory = XPathFactory.newInstance();
5:XPath xpath = factory.newXPath();
6:......
// 
7:XPathExpression expr = xpath.compile("//users/user[login/text()='"+nm+"' and
password/text()='"+pw+"']/home_dir/text()");
// 
8:Object result = expr.evaluate(doc, XPathConstants.NODESET);
// 
9:NodeList nodes = (NodeList) result;
10:for (int i=0; i<nodes.getLength(); i++) {
11: String value = nodes.item(i).getNodeValue();
12: if (value.indexOf(">") < 0) {
// 
13: System.out.println(value);
14: }:
15:}

ㅇ 설명

1. r7에서 nm, pw 값 받은거 검증 안하고 쿼리에 던짐

2. r8에서 추가 검증 없이 result에 저장함

3. 그 이후 코드는 뭘 의미하는지 모르겠음. r13에서 결과를 출력하는데 익스플로잇한 로그인 사용자의 ID/PW?

 

ㅇ 수정

declare variable $loginID as xs:string external;
declare variable $password as xs:string external;
//users/user[@loginID=$loginID and @password=$password]
// XQuery를 이용한 XPath Injection 방지
1:String nm = props.getProperty("name");
2:String pw = props.getProperty("password");
3:Document doc = new Builder().build("users.xml");
//파라미터화된 쿼리가 담겨있는 login.xq를 읽어와서 파라미터화된 쿼리를 생성한다.
4:XQuery xquery = new XQueryFactory().createXQuery(new File("login.xq"));
5:Map vars = new HashMap();
//검증되지 않은 외부값인 nm, pw를 파라미터화된 쿼리의 파라미터로 설정한다.
6:vars.put("loginID", nm);
7:vars.put("password", pw);
// 파라미터화된 쿼리를 실행하므로 외부값을 검증 없이 사용하여도 안전하다.
8:Nodes results = xquery.execute(doc, null, vars).toNodes();
9:for (int i=0; i<results.size(); i++) {
10: System.out.println(results.get(i).toXML());21:
11:}

ㅇ 설명

1. 로그인 쿼리를 불러와서(파라미터화) iD/PW값을 대입하는 방법으로 보임.

2. 아니면 ID/PW값에서 구문 분리 유발 가능성이 있는 문자열 필터링 후 로그인하는 방식 사용도 가능함

 


ㅇ 분석

1:public static void main(String[] args) throws Exception {
2: if (args.length <= 0) {
3: System.err.println("가격을 검색할 식품의 이름을 입력하세요.");
4: return;
5: }
6: String name = args[0];
7: DocumentBuilder docBuilder =DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = docBuilder.parse("http://www.w3schools.com/xml/simple.xml");
8: XPath xpath = XPathFactory.newInstance().newXPath();
// 
// 
9: NodeList nodes = (NodeList) xpath.evaluate("//food[name='" + name + "']/price",
doc, XPathConstants.NODESET);
10: for (int i = 0; i < nodes.getLength(); i++) {
11: System.out.println(nodes.item(i).getTextContent());
12: }
13:}

ㅇ 설명

1. name값에 대한 검증 없이 쿼리 실행

 

ㅇ 수정

1:public static void main(String[] args) throws Exception {
2: if (args.length <= 0) {
3: System.err.println("가격을 검색할 식품의 이름을 입력하세요.");
4: return;
5: }
//프로그램의 커맨드 옵션으로 입력되는 외부값 name에서 XPath 구문을 조작할 수
// 는 문자를 제거하는 검증을 수행하여 안전하다.
6: String name = args[0];
7: if (name != null) {
8: name = name.replaceAll("[()\\-'\\[\\]:,*/]", "");
9: }
10: DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
11: Document doc = docBuilder.parse("http://www.w3schools.com/xml/simple.xml");
12: XPath xpath = XPathFactory.newInstance().newXPath();
13: NodeList nodes = (NodeList) xpath.evaluate("//food[name='" + name + "']/price",
doc, XPathConstants.NODESET);
14: for (int i = 0; i < nodes.getLength(); i++) {
15: System.out.println(nodes.item(i).getTextContent());
16: }

ㅇ 설명

1.  필터링 후 대치 수행

 


ㅇ 분석

1:string acctID = Request["acctID"];
2:string query = null;
3:if(acctID != null)
4:{
5: StringBuffer sb = new StringBuffer("/accounts/account[acctID='");
6: sb.Append(acctID);
7: sb.Append("']/email/text()");
8: query = sb.ToString();
9:}
10: XPathDocument docNav = new XPathDocument(myXml);
11:XPathNavigator nav = docNav.CreateNavigator();
//
12:nav.Evaluate(query);

ㅇ 내용

1.acctID 입력값 검증 안하고 경로에 append해서 사용함.

 

 

ㅇ 수정

1:string xpath = "/accounts/account[@acctID=$acctID]/email/text()";
2:XPathExpression expr = DynamicContext.Compile(xpath);
3:DynamicContext ctx = new DynamicContext();
4:ctx.AddVariable("acctID", AccountIDTextBox.Text);
5:expr.SetContext(ctx);
6:XPathNodeIterator data = nav.Select(expr);

ㅇ 내용

1. 쿼리 형태를 r1과 같이 설정해놓고, acctID값을 받아 사용하여 쿼리구조변경이 없음

 

 

 

 

 

 

 

끝.